sqry_core/config/graph_config_schema.rs
1//! Graph config schema - unified config partition types and validation.
2//!
3//! Implements Step 2 of the Unified Graph Config Partition feature:
4//! - Type definitions for `.sqry/graph/config/config.json`
5//! - Default values for all configuration sections
6//! - Validation logic for schema and values
7//!
8//! # Schema Version
9//!
10//! The schema version is incremented when breaking changes are made to the
11//! config structure. Non-breaking additions (new optional fields) do not
12//! require a version bump.
13//!
14//! # Design
15//!
16//! See: `docs/development/unified-graph-config-partition/02_DESIGN.md`
17
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeMap;
20use thiserror::Error;
21
22use super::project_config::{
23 CacheConfig, IgnoreConfig, IncludeConfig, IndexingConfig, LanguageConfig,
24};
25
26/// Current schema version for config files
27pub const SCHEMA_VERSION: u32 = 1;
28
29/// Errors that can occur during schema validation
30#[derive(Debug, Error)]
31pub enum SchemaValidationError {
32 /// Schema version is incompatible
33 #[error("Incompatible schema version: expected {expected}, found {found}")]
34 IncompatibleVersion {
35 /// Expected schema version
36 expected: u32,
37 /// Actual schema version found
38 found: u32,
39 },
40
41 /// A required field is missing
42 #[error("Missing required field: {0}")]
43 MissingField(String),
44
45 /// A field has an invalid type
46 #[error("Invalid type for field '{field}': expected {expected}, got {got}")]
47 InvalidType {
48 /// Name of the field with invalid type
49 field: String,
50 /// Expected type name
51 expected: String,
52 /// Actual type name found
53 got: String,
54 },
55
56 /// A field has an invalid value
57 #[error("Invalid value for field '{field}': {reason}")]
58 InvalidValue {
59 /// Name of the field with invalid value
60 field: String,
61 /// Reason why the value is invalid
62 reason: String,
63 },
64}
65
66/// Result type for schema validation
67pub type ValidationResult<T> = Result<T, SchemaValidationError>;
68
69// ============================================================================
70// Top-level structure
71// ============================================================================
72
73/// Top-level config file structure
74///
75/// This is the complete structure stored in `.sqry/graph/config/config.json`.
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct GraphConfigFile {
78 /// Schema version for compatibility checking
79 pub schema_version: u32,
80
81 /// Metadata about the config file itself
82 pub metadata: GraphConfigMetadata,
83
84 /// Integrity information for corruption detection
85 pub integrity: GraphConfigIntegrity,
86
87 /// The actual configuration settings
88 pub config: GraphConfig,
89
90 /// Custom extensions for future use
91 #[serde(default)]
92 pub extensions: GraphConfigExtensions,
93}
94
95impl Default for GraphConfigFile {
96 fn default() -> Self {
97 Self {
98 schema_version: SCHEMA_VERSION,
99 metadata: GraphConfigMetadata::default(),
100 integrity: GraphConfigIntegrity::default(),
101 config: GraphConfig::default(),
102 extensions: GraphConfigExtensions::default(),
103 }
104 }
105}
106
107impl GraphConfigFile {
108 /// Create a new config file with default settings
109 #[must_use]
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 /// Validate the schema version is compatible
115 ///
116 /// # Errors
117 ///
118 /// Returns an error if the schema version is incompatible.
119 pub fn validate_version(&self) -> ValidationResult<()> {
120 if self.schema_version != SCHEMA_VERSION {
121 return Err(SchemaValidationError::IncompatibleVersion {
122 expected: SCHEMA_VERSION,
123 found: self.schema_version,
124 });
125 }
126 Ok(())
127 }
128
129 /// Validate the entire config structure
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if any nested config section fails validation.
134 pub fn validate(&self) -> ValidationResult<()> {
135 self.validate_version()?;
136 self.config.validate()?;
137 Ok(())
138 }
139}
140
141// ============================================================================
142// Metadata section
143// ============================================================================
144
145/// Metadata about the config file
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct GraphConfigMetadata {
148 /// When the config file was first created
149 pub created_at: String,
150
151 /// When the config file was last updated
152 pub updated_at: String,
153
154 /// Information about the tool that wrote this config
155 pub written_by: WrittenByInfo,
156}
157
158impl Default for GraphConfigMetadata {
159 fn default() -> Self {
160 let now = chrono::Utc::now().to_rfc3339();
161 Self {
162 created_at: now.clone(),
163 updated_at: now,
164 written_by: WrittenByInfo::default(),
165 }
166 }
167}
168
169/// Information about the tool that wrote the config
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
171pub struct WrittenByInfo {
172 /// sqry version that wrote this config
173 pub sqry_version: String,
174
175 /// Rust compiler version
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub rustc_version: Option<String>,
178
179 /// Git revision (if available)
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub git_revision: Option<String>,
182}
183
184impl Default for WrittenByInfo {
185 fn default() -> Self {
186 Self {
187 sqry_version: env!("CARGO_PKG_VERSION").to_string(),
188 rustc_version: None,
189 git_revision: None,
190 }
191 }
192}
193
194// ============================================================================
195// Integrity section
196// ============================================================================
197
198/// Integrity information for corruption detection
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct GraphConfigIntegrity {
201 /// Hash algorithm used
202 pub normalized_hash_alg: String,
203
204 /// What was hashed (always "config" for now)
205 pub normalized_hash_of: String,
206
207 /// The computed hash (hex-encoded)
208 pub normalized_hash: String,
209
210 /// When the hash was last verified
211 pub last_verified_at: String,
212}
213
214impl Default for GraphConfigIntegrity {
215 fn default() -> Self {
216 Self {
217 normalized_hash_alg: "blake3".to_string(),
218 normalized_hash_of: "config".to_string(),
219 normalized_hash: String::new(), // Computed on save
220 last_verified_at: chrono::Utc::now().to_rfc3339(),
221 }
222 }
223}
224
225// ============================================================================
226// Main config section
227// ============================================================================
228
229/// Main configuration settings
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231#[serde(default)]
232#[derive(Default)]
233pub struct GraphConfig {
234 /// User-defined query aliases
235 pub aliases: BTreeMap<String, AliasEntry>,
236
237 /// CLI preferences
238 pub cli: CliPreferences,
239
240 /// Validation behavior
241 pub validation: ValidationConfig,
242
243 /// Locking behavior
244 pub locking: LockingConfig,
245
246 /// Durability settings
247 pub durability: DurabilityConfig,
248
249 /// Operational limits (configurable, not hard-coded)
250 pub limits: LimitsConfig,
251
252 /// Output formatting settings
253 pub output: OutputConfig,
254
255 /// Parallelism settings
256 pub parallelism: ParallelismConfig,
257
258 /// Timeout settings
259 pub timeouts: TimeoutsConfig,
260
261 /// Graph persistence settings
262 pub persistence: PersistenceConfig,
263
264 /// Indexing configuration (from `ProjectConfig`)
265 #[serde(default)]
266 pub indexing: IndexingConfig,
267
268 /// Cache configuration (from `ProjectConfig`)
269 #[serde(default)]
270 pub cache: CacheConfig,
271
272 /// Language configuration (from `ProjectConfig`)
273 #[serde(default)]
274 pub languages: LanguageConfig,
275
276 /// Include patterns (from `ProjectConfig`)
277 #[serde(default)]
278 pub include: IncludeConfig,
279
280 /// Ignore patterns (from `ProjectConfig`)
281 #[serde(default)]
282 pub ignore: IgnoreConfig,
283
284 /// Buffer size configurations
285 #[serde(default)]
286 pub buffers: BuffersConfig,
287}
288
289impl GraphConfig {
290 /// Validate all config settings
291 ///
292 /// # Errors
293 ///
294 /// Returns an error if any section fails validation.
295 pub fn validate(&self) -> ValidationResult<()> {
296 self.limits.validate()?;
297 self.locking.validate()?;
298 self.output.validate()?;
299 self.timeouts.validate()?;
300 self.persistence.validate()?;
301 Ok(())
302 }
303}
304
305// ============================================================================
306// Alias entry
307// ============================================================================
308
309/// A user-defined query alias
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
311pub struct AliasEntry {
312 /// The query this alias expands to
313 pub query: String,
314
315 /// Optional description of what the alias does
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub description: Option<String>,
318
319 /// When the alias was created
320 pub created_at: String,
321
322 /// When the alias was last updated
323 pub updated_at: String,
324}
325
326impl AliasEntry {
327 /// Create a new alias entry
328 pub fn new(query: impl Into<String>, description: Option<String>) -> Self {
329 let now = chrono::Utc::now().to_rfc3339();
330 Self {
331 query: query.into(),
332 description,
333 created_at: now.clone(),
334 updated_at: now,
335 }
336 }
337}
338
339// ============================================================================
340// CLI preferences
341// ============================================================================
342
343/// CLI behavior preferences
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
345#[serde(default)]
346pub struct CliPreferences {
347 /// Default output format (pretty, json, raw)
348 pub default_output_format: String,
349
350 /// Whether to use JSON output by default
351 pub default_json: bool,
352}
353
354impl Default for CliPreferences {
355 fn default() -> Self {
356 Self {
357 default_output_format: "pretty".to_string(),
358 default_json: false,
359 }
360 }
361}
362
363// ============================================================================
364// Validation config
365// ============================================================================
366
367/// Validation behavior settings
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
369#[serde(default)]
370pub struct ValidationConfig {
371 /// Validation mode: "warn", "strict", or "off"
372 pub mode: String,
373
374 /// Whether to enforce integrity hash matching
375 pub enforce_integrity: bool,
376}
377
378impl Default for ValidationConfig {
379 fn default() -> Self {
380 Self {
381 mode: "warn".to_string(),
382 enforce_integrity: false,
383 }
384 }
385}
386
387// ============================================================================
388// Locking config
389// ============================================================================
390
391/// Writer lock behavior settings
392#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
393#[serde(default)]
394pub struct LockingConfig {
395 /// Timeout for acquiring write lock (milliseconds)
396 pub write_lock_timeout_ms: u64,
397
398 /// Timeout before a lock is considered stale (milliseconds)
399 pub stale_lock_timeout_ms: u64,
400
401 /// Policy for stale locks: "deny", "warn", or "allow"
402 pub stale_takeover_policy: String,
403}
404
405impl Default for LockingConfig {
406 fn default() -> Self {
407 Self {
408 write_lock_timeout_ms: 5000,
409 stale_lock_timeout_ms: 30000,
410 stale_takeover_policy: "allow".to_string(),
411 }
412 }
413}
414
415impl LockingConfig {
416 /// Validate locking config values
417 ///
418 /// # Errors
419 ///
420 /// Returns an error if the takeover policy is invalid.
421 pub fn validate(&self) -> ValidationResult<()> {
422 let valid_policies = ["deny", "warn", "allow"];
423 if !valid_policies.contains(&self.stale_takeover_policy.as_str()) {
424 return Err(SchemaValidationError::InvalidValue {
425 field: "locking.stale_takeover_policy".to_string(),
426 reason: format!(
427 "must be one of {:?}, got '{}'",
428 valid_policies, self.stale_takeover_policy
429 ),
430 });
431 }
432 Ok(())
433 }
434}
435
436// ============================================================================
437// Durability config
438// ============================================================================
439
440/// Durability and filesystem settings
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
442#[serde(default)]
443#[derive(Default)]
444pub struct DurabilityConfig {
445 /// Allow operation on network filesystems (not recommended)
446 pub allow_network_filesystems: bool,
447}
448
449// ============================================================================
450// Limits config (no hard limits - all configurable)
451// ============================================================================
452
453/// Operational limits - all configurable, no hard-coded caps
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
455#[serde(default)]
456pub struct LimitsConfig {
457 /// Maximum number of results to return (0 = unlimited)
458 pub max_results: u64,
459
460 /// Maximum traversal depth for graph queries (0 = unlimited)
461 pub max_depth: u64,
462
463 /// Maximum bytes per file to process (0 = unlimited)
464 pub max_bytes_per_file: u64,
465
466 /// Maximum number of files to index (0 = unlimited)
467 pub max_files: u64,
468
469 /// Maximum repositories/workspaces (0 = unlimited)
470 pub max_repositories: u64,
471
472 /// Maximum query string length (0 = unlimited)
473 pub max_query_length: u64,
474
475 // === Node & Relation Limits ===
476 /// Maximum sample locations to track per cross-language relation (0 = unlimited)
477 pub max_sample_locations: u64,
478
479 /// Maximum cross-language relations per language pair (0 = unlimited)
480 pub max_relations_per_language_pair: u64,
481
482 // === Query Limits ===
483 /// Maximum regex pattern length in queries (0 = unlimited)
484 pub max_regex_length: u64,
485
486 /// Maximum repetition count in query patterns (0 = unlimited)
487 pub max_repetition_count: u64,
488
489 /// Maximum predicates per query (0 = unlimited)
490 pub max_predicates: u64,
491
492 /// Maximum query memory usage in bytes (0 = unlimited)
493 pub max_query_memory_bytes: u64,
494
495 /// Maximum query cost units (0 = unlimited)
496 pub max_query_cost: u64,
497
498 // === Git & External Tools ===
499 /// Maximum git command output size in bytes (0 = unlimited)
500 pub max_git_output_bytes: u64,
501
502 // === Index & Storage Limits ===
503 /// Maximum uncompressed index size in bytes (0 = unlimited)
504 pub max_index_uncompressed_bytes: u64,
505
506 /// Maximum compression ratio for index files (0 = unlimited)
507 pub max_compression_ratio: u64,
508
509 /// Maximum index size in bytes (compressed) (0 = unlimited)
510 pub max_index_bytes: u64,
511
512 // === Prewarm Storage Limits ===
513 /// Maximum prewarm header size in bytes (0 = unlimited)
514 pub max_prewarm_header_bytes: u64,
515
516 /// Maximum prewarm payload size in bytes (0 = unlimited)
517 pub max_prewarm_payload_bytes: u64,
518
519 // === Analysis Limits ===
520 /// Maximum 2-hop label intervals per edge kind (0 = unlimited). Default: 15M.
521 pub analysis_label_budget_per_kind: u64,
522
523 /// Density gate threshold: skip 2-hop labels when `cond_edges > threshold * scc_count`.
524 /// 0 = disabled. Default: 64.
525 pub analysis_density_gate_threshold: u64,
526
527 /// Policy when label budget is exceeded: `"degrade"` (BFS fallback) or `"fail"`.
528 /// Default: `"degrade"`.
529 pub analysis_budget_exceeded_policy: String,
530}
531
532impl Default for LimitsConfig {
533 fn default() -> Self {
534 Self {
535 max_results: 5000,
536 max_depth: 6,
537 max_bytes_per_file: 10 * 1024 * 1024, // 10 MB
538 max_files: 0, // unlimited
539 max_repositories: 0, // unlimited
540 max_query_length: 0, // unlimited
541
542 // Node & Relation Limits
543 max_sample_locations: 3,
544 max_relations_per_language_pair: 10_000,
545
546 // Query Limits
547 max_regex_length: 1000,
548 max_repetition_count: 1000,
549 max_predicates: 100,
550 max_query_memory_bytes: 512 * 1024 * 1024, // 512 MB
551 max_query_cost: 1_000_000,
552
553 // Git & External Tools
554 max_git_output_bytes: 10 * 1024 * 1024, // 10 MB
555
556 // Index & Storage Limits
557 max_index_uncompressed_bytes: 500 * 1024 * 1024, // 500 MB
558 max_compression_ratio: 100,
559 max_index_bytes: 1_000_000_000, // 1 GB
560
561 // Prewarm Storage Limits
562 max_prewarm_header_bytes: 4 * 1024, // 4 KB
563 max_prewarm_payload_bytes: 1024 * 1024 * 1024, // 1 GB
564
565 // Analysis Limits
566 analysis_label_budget_per_kind: 15_000_000,
567 analysis_density_gate_threshold: 64,
568 analysis_budget_exceeded_policy: "degrade".to_string(),
569 }
570 }
571}
572
573impl LimitsConfig {
574 /// Validate limits config - values are always valid (0 = unlimited)
575 ///
576 /// # Errors
577 ///
578 /// Returns an error if limits validation fails.
579 pub fn validate(&self) -> ValidationResult<()> {
580 // All limits are valid - 0 means unlimited
581 Ok(())
582 }
583}
584
585// ============================================================================
586// Output config
587// ============================================================================
588
589/// Output formatting settings
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
591#[serde(default)]
592pub struct OutputConfig {
593 /// Enable pagination by default
594 pub default_pagination: bool,
595
596 /// Default page size for paginated output
597 pub page_size: u64,
598
599 /// Maximum bytes to show in previews
600 pub max_preview_bytes: u64,
601}
602
603impl Default for OutputConfig {
604 fn default() -> Self {
605 Self {
606 default_pagination: true,
607 page_size: 50,
608 max_preview_bytes: 64 * 1024, // 64 KB
609 }
610 }
611}
612
613impl OutputConfig {
614 /// Validate output config values
615 ///
616 /// # Errors
617 ///
618 /// Returns an error if output values are invalid.
619 pub fn validate(&self) -> ValidationResult<()> {
620 if self.page_size == 0 {
621 return Err(SchemaValidationError::InvalidValue {
622 field: "output.page_size".to_string(),
623 reason: "must be greater than 0".to_string(),
624 });
625 }
626 Ok(())
627 }
628}
629
630// ============================================================================
631// Parallelism config
632// ============================================================================
633
634/// Parallelism settings
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636#[serde(default)]
637pub struct ParallelismConfig {
638 /// Maximum threads for parallel operations (0 = auto-detect)
639 pub max_threads: u64,
640
641 /// Maximum lexer pool size (0 = auto-detect)
642 pub lexer_pool_max: u64,
643
644 /// Compaction chunk size for interruptible compaction
645 pub compaction_chunk_size: u64,
646}
647
648impl Default for ParallelismConfig {
649 fn default() -> Self {
650 Self {
651 max_threads: 0, // auto-detect
652 lexer_pool_max: 4,
653 compaction_chunk_size: 10_000,
654 }
655 }
656}
657
658// ============================================================================
659// Timeouts config
660// ============================================================================
661
662/// Timeout settings for various operations
663#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
664#[serde(default)]
665pub struct TimeoutsConfig {
666 /// Query timeout in milliseconds (0 = unlimited)
667 pub query_timeout_ms: u64,
668
669 /// Build/index timeout in milliseconds (0 = unlimited)
670 pub build_timeout_ms: u64,
671
672 /// Parse timeout in microseconds for tree-sitter (0 = unlimited)
673 pub parse_timeout_us: u64,
674
675 /// Session idle timeout in milliseconds (0 = unlimited)
676 pub session_timeout_ms: u64,
677
678 /// File watch debounce timeout in milliseconds (0 = unlimited)
679 pub watch_debounce_ms: u64,
680}
681
682impl Default for TimeoutsConfig {
683 fn default() -> Self {
684 Self {
685 query_timeout_ms: 0, // unlimited
686 build_timeout_ms: 0, // unlimited
687 parse_timeout_us: 2_000_000, // 2 seconds
688 session_timeout_ms: 120_000, // 2 minutes
689 watch_debounce_ms: 50, // 50 milliseconds
690 }
691 }
692}
693
694impl TimeoutsConfig {
695 /// Validate timeout values
696 ///
697 /// # Errors
698 ///
699 /// Returns an error if timeout validation fails.
700 pub fn validate(&self) -> ValidationResult<()> {
701 // 0 means unlimited, all values are valid
702 Ok(())
703 }
704}
705
706// ============================================================================
707// Persistence config
708// ============================================================================
709
710/// Graph persistence size limits
711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
712#[serde(default)]
713pub struct PersistenceConfig {
714 /// Maximum manifest file size in bytes (0 = unlimited)
715 pub max_manifest_bytes: u64,
716
717 /// Maximum snapshot file size in bytes (0 = unlimited)
718 pub max_snapshot_bytes: u64,
719}
720
721impl Default for PersistenceConfig {
722 fn default() -> Self {
723 Self {
724 max_manifest_bytes: 1024 * 1024, // 1 MB
725 max_snapshot_bytes: 0, // unlimited
726 }
727 }
728}
729
730impl PersistenceConfig {
731 /// Validate persistence config values
732 ///
733 /// # Errors
734 ///
735 /// Returns an error if persistence validation fails.
736 pub fn validate(&self) -> ValidationResult<()> {
737 // 0 means unlimited, all values are valid
738 Ok(())
739 }
740}
741
742// ============================================================================
743// Buffers config
744// ============================================================================
745
746/// Buffer size configurations
747///
748/// These were previously environment variables or hard-coded defaults.
749/// Now they're fully configurable.
750#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
751#[serde(default)]
752pub struct BuffersConfig {
753 /// Parse buffer size in bytes
754 pub parse_buffer_bytes: u64,
755
756 /// Node extraction buffer size in bytes
757 pub symbol_buffer_bytes: u64,
758
759 /// LRU cache capacity for parsed ASTs
760 pub ast_cache_capacity: u64,
761
762 /// Query result buffer initial capacity
763 pub query_result_capacity: u64,
764
765 /// Watch event queue capacity
766 pub watch_event_queue_size: u64,
767
768 /// Channel capacity for background workers
769 pub channel_capacity: u64,
770
771 /// Read buffer size in bytes
772 pub read_buffer_bytes: u64,
773
774 /// Write buffer size in bytes
775 pub write_buffer_bytes: u64,
776
777 /// Index buffer size in bytes
778 pub index_buffer_bytes: u64,
779}
780
781impl Default for BuffersConfig {
782 fn default() -> Self {
783 Self {
784 parse_buffer_bytes: 1024 * 1024, // 1 MB
785 symbol_buffer_bytes: 512 * 1024, // 512 KB
786 ast_cache_capacity: 100, // 100 entries
787 query_result_capacity: 1000, // 1000 results
788 watch_event_queue_size: 10_000, // 10k events
789 channel_capacity: 1000, // 1000 items
790 read_buffer_bytes: 8192, // 8 KB
791 write_buffer_bytes: 8192, // 8 KB
792 index_buffer_bytes: 1024 * 1024, // 1 MB
793 }
794 }
795}
796
797// ============================================================================
798// Extensions
799// ============================================================================
800
801/// Custom extensions for future use
802#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
803#[serde(default)]
804pub struct GraphConfigExtensions {
805 /// Custom key-value pairs
806 #[serde(flatten)]
807 pub custom: BTreeMap<String, serde_json::Value>,
808}
809
810// ============================================================================
811// Tests
812// ============================================================================
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 #[test]
819 fn test_defaults_roundtrip() {
820 let config = GraphConfigFile::default();
821 let json = serde_json::to_string_pretty(&config).unwrap();
822 let parsed: GraphConfigFile = serde_json::from_str(&json).unwrap();
823 assert_eq!(config, parsed);
824 }
825
826 #[test]
827 fn test_schema_version_check() {
828 let config = GraphConfigFile::default();
829 assert!(config.validate_version().is_ok());
830
831 #[allow(clippy::field_reassign_with_default)]
832 let old_config = {
833 let mut c = GraphConfigFile::default();
834 c.schema_version = 0;
835 c
836 };
837 assert!(old_config.validate_version().is_err());
838 }
839
840 #[test]
841 fn test_validation_rejects_invalid_policy() {
842 let mut config = GraphConfigFile::default();
843 config.config.locking.stale_takeover_policy = "invalid".to_string();
844 assert!(config.validate().is_err());
845 }
846
847 #[test]
848 fn test_validation_accepts_valid_policies() {
849 for policy in ["deny", "warn", "allow"] {
850 let mut config = GraphConfigFile::default();
851 config.config.locking.stale_takeover_policy = policy.to_string();
852 assert!(config.validate().is_ok());
853 }
854 }
855
856 #[test]
857 fn test_validation_rejects_zero_page_size() {
858 let mut config = GraphConfigFile::default();
859 config.config.output.page_size = 0;
860 assert!(config.validate().is_err());
861 }
862
863 #[test]
864 #[allow(clippy::field_reassign_with_default)]
865 fn test_limits_zero_means_unlimited() {
866 let mut config = LimitsConfig::default();
867 config.max_results = 0;
868 config.max_depth = 0;
869 config.max_bytes_per_file = 0;
870 config.max_files = 0;
871 config.max_repositories = 0;
872 config.max_query_length = 0;
873 assert!(config.validate().is_ok());
874 }
875
876 #[test]
877 fn test_alias_entry_creation() {
878 let alias = AliasEntry::new("kind:function", Some("Find functions".to_string()));
879 assert_eq!(alias.query, "kind:function");
880 assert_eq!(alias.description, Some("Find functions".to_string()));
881 assert!(!alias.created_at.is_empty());
882 assert!(!alias.updated_at.is_empty());
883 }
884
885 #[test]
886 fn test_metadata_timestamps() {
887 let metadata = GraphConfigMetadata::default();
888 assert!(!metadata.created_at.is_empty());
889 assert!(!metadata.updated_at.is_empty());
890 }
891
892 #[test]
893 fn test_written_by_has_version() {
894 let written_by = WrittenByInfo::default();
895 assert!(!written_by.sqry_version.is_empty());
896 }
897
898 #[test]
899 fn test_extensions_empty_by_default() {
900 let ext = GraphConfigExtensions::default();
901 assert!(ext.custom.is_empty());
902 }
903
904 #[test]
905 fn test_full_validation() {
906 let config = GraphConfigFile::default();
907 assert!(config.validate().is_ok());
908 }
909}