1use std::{fs, io, path::PathBuf};
2
3use crate::OrgModeError;
4use config::{
5 Config as ConfigRs, ConfigError, Environment, File,
6 builder::{ConfigBuilder, DefaultState},
7};
8use serde::{Deserialize, Serialize};
9use shellexpand::tilde;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OrgConfig {
14 #[serde(default = "default_org_directory")]
15 pub org_directory: String,
16 #[serde(default = "default_notes_file")]
17 pub org_default_notes_file: String,
18 #[serde(default = "default_agenda_files")]
19 pub org_agenda_files: Vec<String>,
20 #[serde(default)]
21 pub org_agenda_text_search_extra_files: Vec<String>,
22 #[serde(default = "default_todo_keywords")]
23 pub org_todo_keywords: Vec<String>,
24 #[serde(default = "default_org_auto_created_property")]
25 pub org_auto_created_property: bool,
26 #[serde(default = "default_org_auto_closed_timestamp")]
27 pub org_auto_closed_timestamp: bool,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct LoggingConfig {
33 #[serde(default = "default_log_level")]
34 pub level: String,
35 #[serde(default = "default_log_file")]
36 pub file: String,
37}
38
39impl Default for OrgConfig {
40 fn default() -> Self {
41 Self {
42 org_directory: default_org_directory(),
43 org_default_notes_file: default_notes_file(),
44 org_agenda_files: default_agenda_files(),
45 org_agenda_text_search_extra_files: Vec::default(),
46 org_todo_keywords: default_todo_keywords(),
47 org_auto_created_property: default_org_auto_created_property(),
48 org_auto_closed_timestamp: default_org_auto_closed_timestamp(),
49 }
50 }
51}
52
53impl Default for LoggingConfig {
54 fn default() -> Self {
55 Self {
56 level: default_log_level(),
57 file: default_log_file(),
58 }
59 }
60}
61
62impl OrgConfig {
63 pub fn validate(mut self) -> Result<Self, OrgModeError> {
65 let expanded_root = tilde(&self.org_directory);
66 let root_path = PathBuf::from(expanded_root.as_ref());
67
68 if !root_path.exists() {
69 return Err(OrgModeError::ConfigError(format!(
70 "Root directory does not exist: {}",
71 self.org_directory
72 )));
73 }
74
75 if !root_path.is_dir() {
76 return Err(OrgModeError::ConfigError(format!(
77 "Root directory is not a directory: {}",
78 self.org_directory
79 )));
80 }
81
82 if self.org_todo_keywords.len() < 2 {
83 return Err(OrgModeError::ConfigError(
84 "org_todo_keywords must contain at least two keywords".to_string(),
85 ));
86 }
87
88 let separators: Vec<usize> = self
89 .org_todo_keywords
90 .iter()
91 .enumerate()
92 .filter_map(|(i, x)| (x == "|").then_some(i))
93 .collect();
94
95 if separators.len() > 1 {
96 return Err(OrgModeError::ConfigError(
97 "Multiple '|' separators found in org_todo_keywords".to_string(),
98 ));
99 }
100
101 if separators.len() == 1 {
102 let sep_pos = separators[0];
103 if sep_pos == 0 {
104 return Err(OrgModeError::ConfigError(
105 "Separator '|' cannot be at the beginning of org_todo_keywords".to_string(),
106 ));
107 }
108 if sep_pos == self.org_todo_keywords.len() - 1 {
109 return Err(OrgModeError::ConfigError(
110 "Separator '|' cannot be at the end of org_todo_keywords".to_string(),
111 ));
112 }
113 }
114
115 match fs::read_dir(&root_path) {
116 Ok(_) => {}
117 Err(e) => {
118 if e.kind() == io::ErrorKind::PermissionDenied {
119 return Err(OrgModeError::InvalidDirectory(format!(
120 "Permission denied accessing directory: {root_path:?}"
121 )));
122 }
123 return Err(OrgModeError::IoError(e));
124 }
125 }
126
127 self.org_directory = expanded_root.to_string();
128 Ok(self)
129 }
130
131 pub fn unfinished_keywords(&self) -> Vec<String> {
132 if let Some(pos) = self.org_todo_keywords.iter().position(|x| x == "|") {
133 self.org_todo_keywords[..pos].to_vec()
134 } else {
135 self.org_todo_keywords[..self.org_todo_keywords.len() - 1].to_vec()
136 }
137 }
138
139 pub fn finished_keywords(&self) -> Vec<String> {
140 if let Some(pos) = self.org_todo_keywords.iter().position(|x| x == "|") {
141 self.org_todo_keywords[pos + 1..self.org_todo_keywords.len()].to_vec()
142 } else {
143 self.org_todo_keywords
144 .last()
145 .map(|e| vec![e.clone()])
146 .unwrap_or_default()
147 }
148 }
149}
150
151pub fn default_config_path() -> Result<PathBuf, OrgModeError> {
153 Ok(default_config_dir()?.join("config"))
154}
155
156pub fn find_config_file(base_path: PathBuf) -> Option<PathBuf> {
161 if base_path.exists() {
162 return Some(base_path);
163 }
164
165 if let Some(parent) = base_path.parent() {
166 for ext in &["toml", "yaml", "yml", "json"] {
167 let path_with_ext = parent.join(format!("config.{ext}"));
168 if path_with_ext.exists() {
169 return Some(path_with_ext);
170 }
171 }
172 }
173
174 None
175}
176
177pub fn build_config_with_file_and_env(
189 config_file: Option<&str>,
190 builder: ConfigBuilder<DefaultState>,
191) -> Result<ConfigRs, OrgModeError> {
192 let config_path = if let Some(path) = config_file {
193 PathBuf::from(path)
194 } else {
195 default_config_path()?
196 };
197
198 let mut builder = builder;
199 if let Some(config_file_path) = find_config_file(config_path) {
200 builder = builder.add_source(File::from(config_file_path).required(false));
201 }
202
203 builder = builder.add_source(
204 Environment::with_prefix("AGD")
205 .prefix_separator("_")
206 .separator("__"),
207 );
208
209 builder
210 .build()
211 .map_err(|e: ConfigError| OrgModeError::ConfigError(format!("Failed to build config: {e}")))
212}
213
214pub fn load_org_config(
216 config_file: Option<&str>,
217 org_directory: Option<&str>,
218) -> Result<OrgConfig, OrgModeError> {
219 let builder = ConfigRs::builder()
220 .set_default("org.org_directory", default_org_directory())?
221 .set_default("org.org_default_notes_file", default_notes_file())?
222 .set_default("org.org_agenda_files", default_agenda_files())?
223 .set_default(
224 "org.org_auto_created_property",
225 default_org_auto_created_property(),
226 )?
227 .set_default(
228 "org.org_auto_closed_timestamp",
229 default_org_auto_closed_timestamp(),
230 )?;
231
232 let config = build_config_with_file_and_env(config_file, builder)?;
233
234 let mut org_config: OrgConfig = config.get("org").map_err(|e: ConfigError| {
235 OrgModeError::ConfigError(format!("Failed to deserialize org config: {e}"))
236 })?;
237
238 if let Some(org_directory) = org_directory {
239 org_config.org_directory = org_directory.to_string();
240 }
241
242 org_config.validate()
243}
244
245pub fn load_logging_config(
247 config_file: Option<&str>,
248 log_level: Option<&str>,
249) -> Result<LoggingConfig, OrgModeError> {
250 let builder = ConfigRs::builder()
251 .set_default("logging.level", default_log_level())?
252 .set_default("logging.file", default_log_file())?;
253
254 let config = build_config_with_file_and_env(config_file, builder)?;
255
256 let mut config: LoggingConfig = config.get("logging").map_err(|e: ConfigError| {
257 OrgModeError::ConfigError(format!("Failed to deserialize logging config: {e}"))
258 })?;
259
260 if let Some(level) = log_level {
261 config.level = level.to_string();
262 }
263
264 Ok(config)
265}
266
267fn default_config_dir() -> Result<PathBuf, OrgModeError> {
268 let config_dir = dirs::config_dir().ok_or_else(|| {
269 OrgModeError::ConfigError("Could not determine config directory".to_string())
270 })?;
271
272 Ok(config_dir.join("agendum"))
273}
274
275pub fn default_org_directory() -> String {
276 "~/org/".to_string()
277}
278
279pub fn default_notes_file() -> String {
280 "notes.org".to_string()
281}
282
283pub fn default_agenda_files() -> Vec<String> {
284 vec!["**/*.org".to_string()]
285}
286
287pub fn default_todo_keywords() -> Vec<String> {
288 vec!["TODO".to_string(), "|".to_string(), "DONE".to_string()]
289}
290
291pub fn default_log_level() -> String {
292 "info".to_string()
293}
294
295pub fn default_log_file() -> String {
296 "~/.local/share/agendum/logs/agd.log".to_string()
297}
298
299pub fn default_org_auto_created_property() -> bool {
300 true
301}
302
303pub fn default_org_auto_closed_timestamp() -> bool {
304 true
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use serial_test::serial;
311 use temp_env::with_vars;
312 use tempfile::tempdir;
313
314 #[test]
315 fn test_org_auto_created_property_defaults_true() {
316 let config = OrgConfig::default();
317 assert!(config.org_auto_created_property);
318 }
319
320 #[test]
321 fn test_default_org_config() {
322 let config = OrgConfig::default();
323 assert_eq!(config.org_directory, "~/org/");
324 assert_eq!(config.org_default_notes_file, "notes.org");
325 assert_eq!(config.org_agenda_files, vec!["**/*.org"]);
326 assert!(config.org_agenda_text_search_extra_files.is_empty());
327 }
328
329 #[test]
330 fn test_default_logging_config() {
331 let config = LoggingConfig::default();
332 assert_eq!(config.level, "info");
333 assert_eq!(config.file, "~/.local/share/agendum/logs/agd.log");
334 }
335
336 #[test]
337 fn test_config_serialization() {
338 let org_config = OrgConfig::default();
339 let toml_str = toml::to_string_pretty(&org_config).unwrap();
340 let parsed: OrgConfig = toml::from_str(&toml_str).unwrap();
341
342 assert_eq!(org_config.org_directory, parsed.org_directory);
343 assert_eq!(
344 org_config.org_default_notes_file,
345 parsed.org_default_notes_file
346 );
347 assert_eq!(org_config.org_agenda_files, parsed.org_agenda_files);
348 }
349
350 #[test]
351 #[cfg_attr(
352 target_os = "windows",
353 ignore = "Environment variable handling unreliable in Windows tests"
354 )]
355 #[serial]
356 fn test_env_var_override() {
357 let temp_dir = tempdir().unwrap();
358 let temp_path = temp_dir.path().to_str().unwrap();
359
360 with_vars(
361 [
362 ("AGD_ORG__ORG_DIRECTORY", Some(temp_path)),
363 ("AGD_ORG__ORG_DEFAULT_NOTES_FILE", Some("test-notes.org")),
364 ],
365 || {
366 let config = load_org_config(None, None).unwrap();
367 assert_eq!(config.org_directory, temp_path);
368 assert_eq!(config.org_default_notes_file, "test-notes.org");
369 },
370 );
371 }
372
373 #[test]
374 fn test_validate_directory_expansion() {
375 let temp_dir = tempdir().unwrap();
376 let config = OrgConfig {
377 org_directory: temp_dir.path().to_str().unwrap().to_string(),
378 ..OrgConfig::default()
379 };
380
381 let validated = config.validate().unwrap();
382 assert_eq!(validated.org_directory, temp_dir.path().to_str().unwrap());
383 }
384
385 #[test]
386 fn test_validate_nonexistent_directory() {
387 let config = OrgConfig {
388 org_directory: "/nonexistent/test/directory".to_string(),
389 ..OrgConfig::default()
390 };
391
392 let result = config.validate();
393 assert!(result.is_err());
394 match result.unwrap_err() {
395 OrgModeError::ConfigError(msg) => {
396 assert!(msg.contains("Root directory does not exist"));
397 }
398 _ => panic!("Expected ConfigError"),
399 }
400 }
401
402 #[test]
403 fn test_validate_non_directory_path() {
404 let temp_dir = tempdir().unwrap();
405 let file_path = temp_dir.path().join("not-a-dir.txt");
406 std::fs::write(&file_path, "test").unwrap();
407
408 let config = OrgConfig {
409 org_directory: file_path.to_str().unwrap().to_string(),
410 ..OrgConfig::default()
411 };
412
413 let result = config.validate();
414 assert!(result.is_err());
415 match result.unwrap_err() {
416 OrgModeError::ConfigError(msg) => {
417 assert!(msg.contains("not a directory"));
418 }
419 _ => panic!("Expected ConfigError"),
420 }
421 }
422
423 #[test]
424 #[serial]
425 fn test_load_from_toml_file() {
426 let temp_dir = tempdir().unwrap();
427 let path_str = test_utils::config::normalize_path(temp_dir.path());
428 let test_config = format!(
429 r#"
430[org]
431org_directory = "{path_str}"
432org_default_notes_file = "custom-notes.org"
433org_agenda_files = ["test1.org", "test2.org"]
434"#,
435 );
436
437 let config_path = test_utils::config::create_toml_config(&temp_dir, &test_config).unwrap();
438
439 let config = load_org_config(Some(config_path.to_str().unwrap()), None);
440 let config = config.unwrap();
441
442 assert_eq!(config.org_directory, path_str);
443 assert_eq!(config.org_default_notes_file, "custom-notes.org");
444 assert_eq!(config.org_agenda_files, vec!["test1.org", "test2.org"]);
445 }
446
447 #[test]
448 #[serial]
449 fn test_load_from_yaml_file() {
450 let temp_dir = tempdir().unwrap();
451 let path_str = test_utils::config::normalize_path(temp_dir.path());
452 let yaml_config = format!(
453 r#"
454org:
455 org_directory: "{path_str}"
456 org_default_notes_file: "yaml-notes.org"
457 org_agenda_files:
458 - "yaml1.org"
459 - "yaml2.org"
460"#
461 );
462
463 let yaml_path = test_utils::config::create_yaml_config(&temp_dir, &yaml_config).unwrap();
464 let config_dir = yaml_path.parent().unwrap();
465
466 let config = load_org_config(Some(config_dir.join("config").to_str().unwrap()), None);
467 let config = config.unwrap();
468
469 assert_eq!(config.org_directory, path_str);
470 assert_eq!(config.org_default_notes_file, "yaml-notes.org");
471 assert_eq!(config.org_agenda_files, vec!["yaml1.org", "yaml2.org"]);
472 }
473
474 #[test]
475 #[serial]
476 fn test_load_from_yml_file() {
477 let temp_dir = tempdir().unwrap();
478 let path_str = test_utils::config::normalize_path(temp_dir.path());
479 let yml_config = format!(
480 r#"
481org:
482 org_directory: "{path_str}"
483 org_default_notes_file: "yml-notes.org"
484logging:
485 level: "debug"
486 file: "/tmp/test.log"
487"#
488 );
489
490 let yml_path = test_utils::config::create_yml_config(&temp_dir, &yml_config).unwrap();
491 let config_dir = yml_path.parent().unwrap();
492
493 let config = load_org_config(Some(config_dir.join("config").to_str().unwrap()), None);
494 let config = config.unwrap();
495 assert_eq!(config.org_default_notes_file, "yml-notes.org");
496
497 let logging_config =
498 load_logging_config(Some(config_dir.join("config").to_str().unwrap()), None);
499 let logging_config = logging_config.unwrap();
500 assert_eq!(logging_config.level, "debug");
501 assert_eq!(logging_config.file, "/tmp/test.log");
502 }
503
504 #[test]
505 #[serial]
506 fn test_load_from_json_file() {
507 let temp_dir = tempdir().unwrap();
508 let path_str = test_utils::config::normalize_path(temp_dir.path());
509 let json_config = format!(
510 r#"{{
511 "org": {{
512 "org_directory": "{path_str}",
513 "org_default_notes_file": "json-notes.org",
514 "org_agenda_files": ["json1.org", "json2.org"]
515 }}
516}}"#
517 );
518
519 let json_path = test_utils::config::create_json_config(&temp_dir, &json_config).unwrap();
520 let config_dir = json_path.parent().unwrap();
521
522 let config = load_org_config(Some(config_dir.join("config").to_str().unwrap()), None);
523 let config = config.unwrap();
524
525 assert_eq!(config.org_directory, path_str);
526 assert_eq!(config.org_default_notes_file, "json-notes.org");
527 assert_eq!(config.org_agenda_files, vec!["json1.org", "json2.org"]);
528 }
529
530 #[test]
531 #[serial]
532 fn test_logging_config_file_extensions() {
533 let temp_dir = tempdir().unwrap();
534
535 let toml_config = r#"
536[logging]
537level = "trace"
538file = "/var/log/test.log"
539"#;
540
541 let toml_path = test_utils::config::create_toml_config(&temp_dir, toml_config).unwrap();
542
543 let config = load_logging_config(Some(toml_path.to_str().unwrap()), None);
544 let config = config.unwrap();
545
546 assert_eq!(config.level, "trace");
547 assert_eq!(config.file, "/var/log/test.log");
548 }
549
550 #[test]
551 fn test_unfinished_keywords_with_separator() {
552 let config = OrgConfig {
553 org_todo_keywords: vec![
554 "TODO".to_string(),
555 "IN_PROGRESS".to_string(),
556 "|".to_string(),
557 "DONE".to_string(),
558 "CANCELLED".to_string(),
559 ],
560 ..OrgConfig::default()
561 };
562
563 let unfinished = config.unfinished_keywords();
564 assert_eq!(unfinished, vec!["TODO", "IN_PROGRESS"]);
565 }
566
567 #[test]
568 fn test_unfinished_keywords_without_separator() {
569 let config = OrgConfig {
570 org_todo_keywords: vec![
571 "TODO".to_string(),
572 "IN_PROGRESS".to_string(),
573 "DONE".to_string(),
574 ],
575 ..OrgConfig::default()
576 };
577
578 let unfinished = config.unfinished_keywords();
579 assert_eq!(unfinished, vec!["TODO", "IN_PROGRESS"]);
580 }
581
582 #[test]
583 fn test_finished_keywords_with_separator() {
584 let config = OrgConfig {
585 org_todo_keywords: vec![
586 "TODO".to_string(),
587 "|".to_string(),
588 "DONE".to_string(),
589 "CANCELLED".to_string(),
590 ],
591 ..OrgConfig::default()
592 };
593
594 let finished = config.finished_keywords();
595 assert_eq!(finished, vec!["DONE", "CANCELLED"]);
596 }
597
598 #[test]
599 fn test_finished_keywords_without_separator() {
600 let config = OrgConfig {
601 org_todo_keywords: vec!["TODO".to_string(), "DONE".to_string()],
602 ..OrgConfig::default()
603 };
604
605 let finished = config.finished_keywords();
606 assert_eq!(finished, vec!["DONE"]);
607 }
608
609 #[test]
610 fn test_validate_empty_keywords() {
611 let temp_dir = tempdir().unwrap();
612 let config = OrgConfig {
613 org_directory: temp_dir.path().to_str().unwrap().to_string(),
614 org_todo_keywords: vec![],
615 ..OrgConfig::default()
616 };
617
618 let result = config.validate();
619 assert!(result.is_err());
620 match result.unwrap_err() {
621 OrgModeError::ConfigError(msg) => {
622 assert!(msg.contains("must contain at least two keywords"));
623 }
624 _ => panic!("Expected ConfigError"),
625 }
626 }
627
628 #[test]
629 fn test_validate_single_keyword() {
630 let temp_dir = tempdir().unwrap();
631 let config = OrgConfig {
632 org_directory: temp_dir.path().to_str().unwrap().to_string(),
633 org_todo_keywords: vec!["TODO".to_string()],
634 ..OrgConfig::default()
635 };
636
637 let result = config.validate();
638 assert!(result.is_err());
639 match result.unwrap_err() {
640 OrgModeError::ConfigError(msg) => {
641 assert!(msg.contains("must contain at least two keywords"));
642 }
643 _ => panic!("Expected ConfigError"),
644 }
645 }
646
647 #[test]
648 fn test_validate_multiple_separators() {
649 let temp_dir = tempdir().unwrap();
650 let config = OrgConfig {
651 org_directory: temp_dir.path().to_str().unwrap().to_string(),
652 org_todo_keywords: vec![
653 "TODO".to_string(),
654 "|".to_string(),
655 "DONE".to_string(),
656 "|".to_string(),
657 "CANCELLED".to_string(),
658 ],
659 ..OrgConfig::default()
660 };
661
662 let result = config.validate();
663 assert!(result.is_err());
664 match result.unwrap_err() {
665 OrgModeError::ConfigError(msg) => {
666 assert!(msg.contains("Multiple '|' separators"));
667 }
668 _ => panic!("Expected ConfigError"),
669 }
670 }
671
672 #[test]
673 fn test_validate_separator_at_beginning() {
674 let temp_dir = tempdir().unwrap();
675 let config = OrgConfig {
676 org_directory: temp_dir.path().to_str().unwrap().to_string(),
677 org_todo_keywords: vec!["|".to_string(), "DONE".to_string()],
678 ..OrgConfig::default()
679 };
680
681 let result = config.validate();
682 assert!(result.is_err());
683 match result.unwrap_err() {
684 OrgModeError::ConfigError(msg) => {
685 assert!(msg.contains("cannot be at the beginning"));
686 }
687 _ => panic!("Expected ConfigError"),
688 }
689 }
690
691 #[test]
692 fn test_validate_separator_at_end() {
693 let temp_dir = tempdir().unwrap();
694 let config = OrgConfig {
695 org_directory: temp_dir.path().to_str().unwrap().to_string(),
696 org_todo_keywords: vec!["TODO".to_string(), "|".to_string()],
697 ..OrgConfig::default()
698 };
699
700 let result = config.validate();
701 assert!(result.is_err());
702 match result.unwrap_err() {
703 OrgModeError::ConfigError(msg) => {
704 assert!(msg.contains("cannot be at the end"));
705 }
706 _ => panic!("Expected ConfigError"),
707 }
708 }
709
710 #[test]
711 fn test_validate_only_separator() {
712 let temp_dir = tempdir().unwrap();
713 let config = OrgConfig {
714 org_directory: temp_dir.path().to_str().unwrap().to_string(),
715 org_todo_keywords: vec!["|".to_string()],
716 ..OrgConfig::default()
717 };
718
719 let result = config.validate();
720 assert!(result.is_err());
721 match result.unwrap_err() {
722 OrgModeError::ConfigError(msg) => {
723 assert!(msg.contains("must contain at least two keywords"));
724 }
725 _ => panic!("Expected ConfigError"),
726 }
727 }
728
729 #[test]
730 fn test_validate_valid_keywords_with_separator() {
731 let temp_dir = tempdir().unwrap();
732 let config = OrgConfig {
733 org_directory: temp_dir.path().to_str().unwrap().to_string(),
734 org_todo_keywords: vec!["TODO".to_string(), "|".to_string(), "DONE".to_string()],
735 ..OrgConfig::default()
736 };
737
738 let result = config.validate();
739 assert!(result.is_ok());
740 }
741
742 #[test]
743 fn test_validate_valid_keywords_without_separator() {
744 let temp_dir = tempdir().unwrap();
745 let config = OrgConfig {
746 org_directory: temp_dir.path().to_str().unwrap().to_string(),
747 org_todo_keywords: vec!["TODO".to_string(), "DONE".to_string()],
748 ..OrgConfig::default()
749 };
750
751 let result = config.validate();
752 assert!(result.is_ok());
753 }
754
755 #[test]
756 fn test_validate_multiple_unfinished_and_finished() {
757 let temp_dir = tempdir().unwrap();
758 let config = OrgConfig {
759 org_directory: temp_dir.path().to_str().unwrap().to_string(),
760 org_todo_keywords: vec![
761 "TODO".to_string(),
762 "IN_PROGRESS".to_string(),
763 "|".to_string(),
764 "DONE".to_string(),
765 "CANCELLED".to_string(),
766 ],
767 ..OrgConfig::default()
768 };
769
770 let result = config.validate();
771 assert!(result.is_ok());
772 }
773
774 #[test]
775 fn test_org_auto_closed_timestamp_defaults_true() {
776 let config = OrgConfig::default();
777 assert!(config.org_auto_closed_timestamp);
778 }
779
780 #[test]
781 #[serial]
782 fn test_load_auto_closed_timestamp_from_toml() {
783 let temp_dir = tempdir().unwrap();
784 let path_str = test_utils::config::normalize_path(temp_dir.path());
785 let toml_config = format!(
786 r#"
787[org]
788org_directory = "{path_str}"
789org_auto_closed_timestamp = false
790"#,
791 );
792
793 let config_path = test_utils::config::create_toml_config(&temp_dir, &toml_config).unwrap();
794
795 let config = load_org_config(Some(config_path.to_str().unwrap()), None).unwrap();
796 assert!(!config.org_auto_closed_timestamp);
797 }
798}