Skip to main content

sqry_core/config/
graph_config_store.rs

1//! Graph config store - unified config partition under `.sqry/graph/config/`
2//!
3//! Implements Step 1 of the Unified Graph Config Partition feature:
4//! - Path resolution for config files and directories
5//! - Network filesystem detection
6//! - Foundation for atomic config operations
7//!
8//! # Storage Layout
9//!
10//! Under `<project_root>/.sqry/graph/`:
11//! - `config/config.json` - canonical config file
12//! - `config/config.json.previous` - last known-good config
13//! - `config/config.json.corrupt.<timestamp>` - quarantined corrupt files
14//! - `config/config.lock` - advisory lock file
15//!
16//! # Design
17//!
18//! See: `docs/development/unified-graph-config-partition/02_DESIGN.md`
19
20use std::path::{Path, PathBuf};
21use thiserror::Error;
22
23/// Errors that can occur with graph config operations
24#[derive(Debug, Error)]
25pub enum GraphConfigError {
26    /// Config directory not initialized
27    #[error("Config directory not found at {0}. Run `sqry config init` to create it.")]
28    NotInitialized(PathBuf),
29
30    /// Network filesystem detected
31    #[error(
32        "Network filesystem detected at {0}. Config operations may be unreliable. Set config.durability.allow_network_filesystems=true to proceed."
33    )]
34    NetworkFilesystem(PathBuf),
35
36    /// IO error
37    #[error("IO error at {0}: {1}")]
38    IoError(PathBuf, #[source] std::io::Error),
39
40    /// Invalid path
41    #[error("Invalid path: {0}")]
42    InvalidPath(String),
43}
44
45#[cfg(target_os = "linux")]
46const NFS_SUPER_MAGIC: i128 = 0x6969;
47#[cfg(target_os = "linux")]
48const SMB_SUPER_MAGIC: i128 = 0x517B;
49#[cfg(target_os = "linux")]
50const CIFS_MAGIC_NUMBER: i128 = 0xFF53_4D42;
51#[cfg(target_os = "linux")]
52const AFS_SUPER_MAGIC: i128 = 0x5346_414F;
53#[cfg(target_os = "linux")]
54const CODA_SUPER_MAGIC: i128 = 0x7375_7245;
55
56/// Result type for graph config operations
57pub type Result<T> = std::result::Result<T, GraphConfigError>;
58
59/// Path resolver for graph config files
60///
61/// Provides canonical paths for all config-related files and directories
62/// under `.sqry/graph/config/`.
63///
64/// # Example
65///
66/// ```rust,ignore
67/// use sqry_core::config::GraphConfigPaths;
68///
69/// let paths = GraphConfigPaths::new("/home/user/project")?;
70/// let config_file = paths.config_file();
71/// let lock_file = paths.lock_file();
72/// ```
73#[derive(Debug, Clone)]
74pub struct GraphConfigPaths {
75    /// Project root directory
76    project_root: PathBuf,
77    /// Override for graph directory (for testing)
78    graph_dir_override: Option<PathBuf>,
79}
80
81impl GraphConfigPaths {
82    /// Create a new path resolver from a project root
83    ///
84    /// # Arguments
85    ///
86    /// * `project_root` - Root directory of the project
87    ///
88    /// # Errors
89    ///
90    /// Returns error if the path is invalid or inaccessible
91    pub fn new<P: AsRef<Path>>(project_root: P) -> Result<Self> {
92        let project_root = project_root.as_ref();
93
94        // Validate path exists and is a directory
95        if !project_root.exists() {
96            return Err(GraphConfigError::InvalidPath(format!(
97                "Project root does not exist: {}",
98                project_root.display()
99            )));
100        }
101
102        if !project_root.is_dir() {
103            return Err(GraphConfigError::InvalidPath(format!(
104                "Project root is not a directory: {}",
105                project_root.display()
106            )));
107        }
108
109        Ok(Self {
110            project_root: project_root.to_path_buf(),
111            graph_dir_override: None,
112        })
113    }
114
115    /// Create path resolver with an explicit graph directory override
116    ///
117    /// This is primarily for testing or when the graph directory is in
118    /// a non-standard location.
119    ///
120    /// # Arguments
121    ///
122    /// * `project_root` - Root directory of the project
123    /// * `graph_dir` - Override path for `.sqry/graph` directory
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the project root is invalid or inaccessible.
128    pub fn with_graph_dir<P: AsRef<Path>, G: AsRef<Path>>(
129        project_root: P,
130        graph_dir: G,
131    ) -> Result<Self> {
132        let mut paths = Self::new(project_root)?;
133        paths.graph_dir_override = Some(graph_dir.as_ref().to_path_buf());
134        Ok(paths)
135    }
136
137    /// Get the graph directory path (`.sqry/graph`)
138    #[must_use]
139    pub fn graph_dir(&self) -> PathBuf {
140        self.graph_dir_override
141            .clone()
142            .unwrap_or_else(|| self.project_root.join(".sqry").join("graph"))
143    }
144
145    /// Get the config directory path (`.sqry/graph/config`)
146    #[must_use]
147    pub fn config_dir(&self) -> PathBuf {
148        self.graph_dir().join("config")
149    }
150
151    /// Get the canonical config file path (`.sqry/graph/config/config.json`)
152    #[must_use]
153    pub fn config_file(&self) -> PathBuf {
154        self.config_dir().join("config.json")
155    }
156
157    /// Get the previous config file path (`.sqry/graph/config/config.json.previous`)
158    #[must_use]
159    pub fn previous_file(&self) -> PathBuf {
160        self.config_dir().join("config.json.previous")
161    }
162
163    /// Get the lock file path (`.sqry/graph/config/config.lock`)
164    #[must_use]
165    pub fn lock_file(&self) -> PathBuf {
166        self.config_dir().join("config.lock")
167    }
168
169    /// Generate a corrupt quarantine file path with timestamp
170    ///
171    /// Format: `.sqry/graph/config/config.json.corrupt.<timestamp>`
172    ///
173    /// # Arguments
174    ///
175    /// * `timestamp` - UTC timestamp in RFC3339 format
176    #[must_use]
177    pub fn corrupt_file(&self, timestamp: &str) -> PathBuf {
178        self.config_dir()
179            .join(format!("config.json.corrupt.{timestamp}"))
180    }
181
182    /// Check if the config directory exists
183    ///
184    /// Returns `true` if `.sqry/graph/config/` exists and is a directory.
185    #[must_use]
186    pub fn config_dir_exists(&self) -> bool {
187        let config_dir = self.config_dir();
188        config_dir.exists() && config_dir.is_dir()
189    }
190
191    /// Check if the config file exists
192    ///
193    /// Returns `true` if `.sqry/graph/config/config.json` exists.
194    #[must_use]
195    pub fn config_file_exists(&self) -> bool {
196        self.config_file().exists()
197    }
198
199    /// Detect if the path is on a network filesystem
200    ///
201    /// Network filesystems (NFS, CIFS, SMB) can violate atomic rename and
202    /// fsync durability assumptions. This detection emits warnings to help
203    /// users avoid data loss scenarios.
204    ///
205    /// # Platform Support
206    ///
207    /// - Linux: Uses `statfs` with `f_type` magic numbers (NFS, SMB, CIFS, AFS, CODA)
208    /// - macOS: Uses `statfs` with `f_fstypename` string (nfs, smbfs, afpfs, webdav, ftp)
209    /// - Windows: UNC path detection + `GetDriveTypeW` for mapped network drives
210    /// - Other platforms: Returns `Ok(false)` (assumes local)
211    ///
212    /// # Returns
213    ///
214    /// Returns `Ok(true)` if network filesystem detected, `Ok(false)` otherwise.
215    /// On Linux, returns an error if the path doesn't exist and no ancestor can be found.
216    /// On macOS and Windows, returns `Ok(false)` on syscall/API failure (safe default).
217    ///
218    /// # Errors
219    ///
220    /// Returns an error on Linux if filesystem inspection fails and no ancestor
221    /// path exists. macOS and Windows implementations prefer `Ok(false)` over errors.
222    pub fn is_network_filesystem(&self) -> Result<bool> {
223        let path = self.graph_dir();
224
225        #[cfg(target_os = "linux")]
226        {
227            Self::is_network_filesystem_linux(&path)
228        }
229
230        #[cfg(target_os = "macos")]
231        {
232            self.is_network_filesystem_macos(&path)
233        }
234
235        #[cfg(windows)]
236        {
237            self.is_network_filesystem_windows(&path)
238        }
239
240        #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
241        {
242            log::debug!(
243                "Network filesystem detection not implemented for this platform. \
244                 Assuming local filesystem at {}",
245                path.display()
246            );
247            Ok(false)
248        }
249    }
250
251    /// Linux-specific network filesystem detection using statfs
252    #[cfg(target_os = "linux")]
253    fn is_network_filesystem_linux(path: &Path) -> Result<bool> {
254        use std::ffi::CString;
255
256        let path_cstr = CString::new(path.to_string_lossy().as_bytes())
257            .map_err(|e| GraphConfigError::InvalidPath(e.to_string()))?;
258
259        let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
260
261        let result = unsafe { libc::statfs(path_cstr.as_ptr(), &raw mut stat) };
262
263        if result != 0 {
264            let err = std::io::Error::last_os_error();
265            // If path doesn't exist yet (ENOENT), walk up to find an existing ancestor
266            if err.kind() == std::io::ErrorKind::NotFound {
267                let mut current = path.parent();
268                while let Some(parent) = current {
269                    if parent.exists() {
270                        return Self::is_network_filesystem_linux(parent);
271                    }
272                    current = parent.parent();
273                }
274            }
275            return Err(GraphConfigError::IoError(path.to_path_buf(), err));
276        }
277
278        // libc varies here: glibc uses signed word-sized values, musl uses
279        // unsigned longs, and some Linux targets use narrower unsigned types.
280        // Normalize to a single wide integer before comparing magic numbers.
281        let fs_type = i128::from(stat.f_type);
282        let is_network = matches!(
283            fs_type,
284            NFS_SUPER_MAGIC
285                | SMB_SUPER_MAGIC
286                | CIFS_MAGIC_NUMBER
287                | AFS_SUPER_MAGIC
288                | CODA_SUPER_MAGIC
289        );
290
291        if is_network {
292            log::warn!(
293                "Network filesystem detected at {} (type: 0x{:X}). \
294                 Config operations may be unreliable. Consider using a local filesystem.",
295                path.display(),
296                fs_type
297            );
298        }
299
300        Ok(is_network)
301    }
302
303    /// macOS-specific network filesystem detection using `statfs` with `f_fstypename`
304    ///
305    /// Unlike Linux (which uses `f_type` integer magic numbers), macOS `statfs`
306    /// provides a human-readable `f_fstypename` char array identifying the filesystem.
307    #[cfg(target_os = "macos")]
308    fn is_network_filesystem_macos(&self, path: &Path) -> Result<bool> {
309        use std::ffi::CString;
310        use std::mem::MaybeUninit;
311
312        // Network filesystem type names on macOS (from f_fstypename)
313        const NETWORK_FS_TYPES: &[&str] = &[
314            "nfs",    // NFS
315            "smbfs",  // SMB/CIFS
316            "afpfs",  // AFP (legacy)
317            "webdav", // WebDAV
318            "ftp",    // FTP mounts
319        ];
320
321        // Handle non-existent paths by checking ancestors (mirrors Linux behavior)
322        let check_path = if path.exists() {
323            path.to_path_buf()
324        } else {
325            path.ancestors()
326                .find(|p| p.exists())
327                .map(Path::to_path_buf)
328                .unwrap_or_else(|| PathBuf::from("/"))
329        };
330
331        let c_path = CString::new(check_path.as_os_str().as_encoded_bytes())
332            .map_err(|e| GraphConfigError::InvalidPath(e.to_string()))?;
333        let mut stat: MaybeUninit<libc::statfs> = MaybeUninit::uninit();
334
335        let result = unsafe { libc::statfs(c_path.as_ptr(), stat.as_mut_ptr()) };
336        if result != 0 {
337            // On error, assume local (safe default)
338            return Ok(false);
339        }
340
341        let stat = unsafe { stat.assume_init() };
342        let fs_type = unsafe {
343            std::ffi::CStr::from_ptr(stat.f_fstypename.as_ptr())
344                .to_string_lossy()
345                .to_lowercase()
346        };
347
348        let is_network = NETWORK_FS_TYPES.iter().any(|&t| fs_type.contains(t));
349
350        if is_network {
351            log::warn!(
352                "Network filesystem detected at {} (type: {}). \
353                 Config operations may be unreliable. Consider using a local filesystem.",
354                path.display(),
355                fs_type
356            );
357        }
358
359        Ok(is_network)
360    }
361
362    /// Windows-specific network filesystem detection
363    ///
364    /// Uses two complementary strategies:
365    /// - UNC path detection (`\\server\share`, `\\?\UNC\...`) via `Path::components()`
366    /// - Mapped network drive detection (`X:\`) via `GetDriveTypeW` (`DRIVE_REMOTE` = 4)
367    #[cfg(windows)]
368    fn is_network_filesystem_windows(&self, path: &Path) -> Result<bool> {
369        use std::path::{Component, Prefix};
370
371        let first_component = path.components().next();
372
373        if let Some(Component::Prefix(prefix_component)) = first_component {
374            match prefix_component.kind() {
375                // UNC paths are network shares
376                Prefix::UNC(_, _) | Prefix::VerbatimUNC(_, _) => {
377                    log::warn!(
378                        "Network filesystem detected at {} (UNC path). \
379                         Config operations may be unreliable. Consider using a local filesystem.",
380                        path.display()
381                    );
382                    return Ok(true);
383                }
384                // Disk prefixes need GetDriveTypeW check for mapped network drives
385                Prefix::Disk(_) | Prefix::VerbatimDisk(_) => {
386                    let root = format!("{}\\", prefix_component.as_os_str().to_string_lossy());
387                    let wide_path: Vec<u16> =
388                        root.encode_utf16().chain(std::iter::once(0)).collect();
389                    let drive_type = unsafe {
390                        windows_sys::Win32::Storage::FileSystem::GetDriveTypeW(wide_path.as_ptr())
391                    };
392                    // DRIVE_REMOTE = 4
393                    let is_network = drive_type == 4;
394                    if is_network {
395                        log::warn!(
396                            "Network filesystem detected at {} (mapped network drive). \
397                             Config operations may be unreliable. Consider using a local filesystem.",
398                            path.display()
399                        );
400                    }
401                    return Ok(is_network);
402                }
403                // Device namespace paths (\\.\...) and verbatim local paths (\\?\...) are local
404                Prefix::DeviceNS(_) | Prefix::Verbatim(_) => {
405                    return Ok(false);
406                }
407            }
408        }
409
410        // Relative paths or root-only paths: assume local
411        Ok(false)
412    }
413
414    /// Validate that the config directory is suitable for operations
415    ///
416    /// Checks:
417    /// - Config directory exists (or can be created)
418    /// - Not on a network filesystem (unless explicitly allowed)
419    ///
420    /// # Arguments
421    ///
422    /// * `allow_network_fs` - If true, skip network filesystem check
423    ///
424    /// # Errors
425    ///
426    /// Returns error if validation fails
427    pub fn validate(&self, allow_network_fs: bool) -> Result<()> {
428        // Check network filesystem if not allowed
429        if !allow_network_fs && self.is_network_filesystem()? {
430            return Err(GraphConfigError::NetworkFilesystem(self.graph_dir()));
431        }
432
433        Ok(())
434    }
435}
436
437/// Graph config store - main API for config operations
438///
439/// Provides the high-level API for loading, saving, and managing
440/// the unified config partition under `.sqry/graph/config/`.
441///
442/// # Example
443///
444/// ```rust,ignore
445/// use sqry_core::config::GraphConfigStore;
446///
447/// let store = GraphConfigStore::new("/home/user/project")?;
448/// store.validate(false)?;
449/// ```
450#[derive(Debug)]
451pub struct GraphConfigStore {
452    paths: GraphConfigPaths,
453}
454
455impl GraphConfigStore {
456    /// Create a new config store for the given project root
457    ///
458    /// # Arguments
459    ///
460    /// * `project_root` - Root directory of the project
461    ///
462    /// # Errors
463    ///
464    /// Returns error if the project root is invalid
465    pub fn new<P: AsRef<Path>>(project_root: P) -> Result<Self> {
466        Ok(Self {
467            paths: GraphConfigPaths::new(project_root)?,
468        })
469    }
470
471    /// Create a new config store with explicit graph directory override
472    ///
473    /// # Arguments
474    ///
475    /// * `project_root` - Root directory of the project
476    /// * `graph_dir` - Override path for `.sqry/graph` directory
477    ///
478    /// # Errors
479    ///
480    /// Returns an error if the project root is invalid or inaccessible.
481    pub fn with_graph_dir<P: AsRef<Path>, G: AsRef<Path>>(
482        project_root: P,
483        graph_dir: G,
484    ) -> Result<Self> {
485        Ok(Self {
486            paths: GraphConfigPaths::with_graph_dir(project_root, graph_dir)?,
487        })
488    }
489
490    /// Get the path resolver
491    #[must_use]
492    pub fn paths(&self) -> &GraphConfigPaths {
493        &self.paths
494    }
495
496    /// Validate the store is ready for operations
497    ///
498    /// # Arguments
499    ///
500    /// * `allow_network_fs` - If true, allow network filesystems (best-effort)
501    ///
502    /// # Errors
503    ///
504    /// Returns error if validation fails
505    pub fn validate(&self, allow_network_fs: bool) -> Result<()> {
506        self.paths.validate(allow_network_fs)
507    }
508
509    /// Check if the config directory is initialized
510    #[must_use]
511    pub fn is_initialized(&self) -> bool {
512        self.paths.config_dir_exists()
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use tempfile::TempDir;
520
521    #[test]
522    fn test_new_with_valid_path() {
523        let temp = TempDir::new().unwrap();
524        let paths = GraphConfigPaths::new(temp.path()).unwrap();
525
526        assert_eq!(paths.project_root, temp.path());
527    }
528
529    #[test]
530    fn test_new_with_nonexistent_path() {
531        let result = GraphConfigPaths::new("/nonexistent/path/that/does/not/exist");
532        assert!(result.is_err());
533        assert!(matches!(result, Err(GraphConfigError::InvalidPath(_))));
534    }
535
536    #[test]
537    fn test_graph_dir_default() {
538        let temp = TempDir::new().unwrap();
539        let paths = GraphConfigPaths::new(temp.path()).unwrap();
540
541        let expected = temp.path().join(".sqry").join("graph");
542        assert_eq!(paths.graph_dir(), expected);
543    }
544
545    #[test]
546    fn test_graph_dir_override() {
547        let temp = TempDir::new().unwrap();
548        let override_dir = temp.path().join("custom-graph");
549        std::fs::create_dir_all(&override_dir).unwrap();
550
551        let paths = GraphConfigPaths::with_graph_dir(temp.path(), &override_dir).unwrap();
552
553        assert_eq!(paths.graph_dir(), override_dir);
554    }
555
556    #[test]
557    fn test_config_dir_path() {
558        let temp = TempDir::new().unwrap();
559        let paths = GraphConfigPaths::new(temp.path()).unwrap();
560
561        let expected = temp.path().join(".sqry").join("graph").join("config");
562        assert_eq!(paths.config_dir(), expected);
563    }
564
565    #[test]
566    fn test_config_file_path() {
567        let temp = TempDir::new().unwrap();
568        let paths = GraphConfigPaths::new(temp.path()).unwrap();
569
570        let expected = temp
571            .path()
572            .join(".sqry")
573            .join("graph")
574            .join("config")
575            .join("config.json");
576        assert_eq!(paths.config_file(), expected);
577    }
578
579    #[test]
580    fn test_previous_file_path() {
581        let temp = TempDir::new().unwrap();
582        let paths = GraphConfigPaths::new(temp.path()).unwrap();
583
584        let expected = temp
585            .path()
586            .join(".sqry")
587            .join("graph")
588            .join("config")
589            .join("config.json.previous");
590        assert_eq!(paths.previous_file(), expected);
591    }
592
593    #[test]
594    fn test_lock_file_path() {
595        let temp = TempDir::new().unwrap();
596        let paths = GraphConfigPaths::new(temp.path()).unwrap();
597
598        let expected = temp
599            .path()
600            .join(".sqry")
601            .join("graph")
602            .join("config")
603            .join("config.lock");
604        assert_eq!(paths.lock_file(), expected);
605    }
606
607    #[test]
608    fn test_corrupt_file_path() {
609        let temp = TempDir::new().unwrap();
610        let paths = GraphConfigPaths::new(temp.path()).unwrap();
611
612        let timestamp = "2025-12-15T21:30:00Z";
613        let expected = temp
614            .path()
615            .join(".sqry")
616            .join("graph")
617            .join("config")
618            .join(format!("config.json.corrupt.{timestamp}"));
619        assert_eq!(paths.corrupt_file(timestamp), expected);
620    }
621
622    #[test]
623    fn test_config_dir_exists_false_when_not_created() {
624        let temp = TempDir::new().unwrap();
625        let paths = GraphConfigPaths::new(temp.path()).unwrap();
626
627        assert!(!paths.config_dir_exists());
628    }
629
630    #[test]
631    fn test_config_dir_exists_true_when_created() {
632        let temp = TempDir::new().unwrap();
633        let paths = GraphConfigPaths::new(temp.path()).unwrap();
634
635        // Create the config directory
636        std::fs::create_dir_all(paths.config_dir()).unwrap();
637
638        assert!(paths.config_dir_exists());
639    }
640
641    #[test]
642    fn test_config_file_exists() {
643        let temp = TempDir::new().unwrap();
644        let paths = GraphConfigPaths::new(temp.path()).unwrap();
645
646        // Initially doesn't exist
647        assert!(!paths.config_file_exists());
648
649        // Create config file
650        std::fs::create_dir_all(paths.config_dir()).unwrap();
651        std::fs::write(paths.config_file(), "{}").unwrap();
652
653        assert!(paths.config_file_exists());
654    }
655
656    #[test]
657    fn test_validate_missing_config_dir_ok_for_init() {
658        let temp = TempDir::new().unwrap();
659        let paths = GraphConfigPaths::new(temp.path()).unwrap();
660
661        // Validation should pass even if config dir doesn't exist yet
662        // (it will be created during init)
663        let result = paths.validate(false);
664        assert!(result.is_ok());
665    }
666
667    #[test]
668    #[cfg(target_os = "linux")]
669    fn test_is_network_filesystem_on_local() {
670        let temp = TempDir::new().unwrap();
671        let paths = GraphConfigPaths::new(temp.path()).unwrap();
672
673        // tempfile should be on local filesystem
674        let result = paths.is_network_filesystem();
675        assert!(result.is_ok());
676        assert!(!result.unwrap());
677    }
678
679    #[test]
680    #[cfg(target_os = "linux")]
681    fn test_is_network_filesystem_with_nonexistent_path() {
682        let temp = TempDir::new().unwrap();
683        let paths = GraphConfigPaths::new(temp.path()).unwrap();
684
685        // Should check parent directory when path doesn't exist
686        let result = paths.is_network_filesystem();
687        assert!(result.is_ok());
688    }
689
690    #[test]
691    #[cfg(target_os = "macos")]
692    fn test_is_network_filesystem_local_on_macos() {
693        let temp = TempDir::new().unwrap();
694        let paths = GraphConfigPaths::new(temp.path()).unwrap();
695
696        // tempfile should be on local filesystem (APFS/HFS+)
697        let result = paths.is_network_filesystem();
698        assert!(result.is_ok());
699        assert!(!result.unwrap());
700    }
701
702    #[test]
703    #[cfg(target_os = "macos")]
704    fn test_is_network_filesystem_nonexistent_path_macos() {
705        let temp = TempDir::new().unwrap();
706        // Use a graph dir override pointing to a non-existent subdirectory
707        let nonexistent = temp.path().join("does").join("not").join("exist");
708        let paths = GraphConfigPaths::with_graph_dir(temp.path(), &nonexistent).unwrap();
709
710        // Ancestor fallback should find the temp directory (local filesystem)
711        let result = paths.is_network_filesystem();
712        assert!(result.is_ok());
713        assert!(!result.unwrap());
714    }
715
716    #[test]
717    #[cfg(windows)]
718    fn test_is_network_filesystem_local_on_windows() {
719        let temp = TempDir::new().unwrap();
720        let paths = GraphConfigPaths::new(temp.path()).unwrap();
721
722        // tempfile should be on a local drive
723        let result = paths.is_network_filesystem();
724        assert!(result.is_ok());
725        assert!(!result.unwrap());
726    }
727
728    #[test]
729    #[cfg(windows)]
730    fn test_is_network_filesystem_unc_path() {
731        let temp = TempDir::new().unwrap();
732        // Override graph dir with a UNC-style path
733        let unc_path = PathBuf::from(r"\\server\share\project\.sqry\graph");
734        let paths = GraphConfigPaths {
735            project_root: temp.path().to_path_buf(),
736            graph_dir_override: Some(unc_path),
737        };
738
739        // UNC paths should be detected as network filesystems
740        let result = paths.is_network_filesystem();
741        assert!(result.is_ok());
742        assert!(result.unwrap());
743    }
744
745    #[test]
746    fn test_store_new() {
747        let temp = TempDir::new().unwrap();
748        let store = GraphConfigStore::new(temp.path()).unwrap();
749
750        assert_eq!(store.paths.project_root, temp.path());
751    }
752
753    #[test]
754    fn test_store_with_graph_dir() {
755        let temp = TempDir::new().unwrap();
756        let override_dir = temp.path().join("custom");
757        std::fs::create_dir_all(&override_dir).unwrap();
758
759        let store = GraphConfigStore::with_graph_dir(temp.path(), &override_dir).unwrap();
760
761        assert_eq!(store.paths.graph_dir(), override_dir);
762    }
763
764    #[test]
765    fn test_store_is_initialized() {
766        let temp = TempDir::new().unwrap();
767        let store = GraphConfigStore::new(temp.path()).unwrap();
768
769        // Initially not initialized
770        assert!(!store.is_initialized());
771
772        // Create config directory
773        std::fs::create_dir_all(store.paths.config_dir()).unwrap();
774
775        assert!(store.is_initialized());
776    }
777
778    #[test]
779    fn test_store_validate() {
780        let temp = TempDir::new().unwrap();
781        let store = GraphConfigStore::new(temp.path()).unwrap();
782
783        // Validation should pass for local filesystem
784        let result = store.validate(false);
785        assert!(result.is_ok());
786    }
787
788    #[test]
789    fn test_paths_accessor() {
790        let temp = TempDir::new().unwrap();
791        let store = GraphConfigStore::new(temp.path()).unwrap();
792
793        let paths = store.paths();
794        assert_eq!(paths.project_root, temp.path());
795    }
796}