1use crate::dal::database::configuration_repository::ConfigurationRepository;
2use crate::domain::configuration::FlightLevelConfig;
3use crate::domain::documents::initiative::Complexity;
4use crate::domain::documents::strategy::RiskLevel;
5use crate::domain::documents::traits::Document;
6use crate::domain::documents::types::{DocumentId, DocumentType, ParentReference, Phase, Tag};
7use crate::Result;
8use crate::{Adr, Database, Initiative, MetisError, Strategy, Task, Vision};
9use diesel::{sqlite::SqliteConnection, Connection};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub struct DocumentCreationService {
15 workspace_dir: PathBuf,
16 db_path: PathBuf,
17}
18
19#[derive(Debug, Clone)]
21pub struct DocumentCreationConfig {
22 pub title: String,
23 pub description: Option<String>,
24 pub parent_id: Option<DocumentId>,
25 pub tags: Vec<Tag>,
26 pub phase: Option<Phase>,
27 pub complexity: Option<Complexity>,
28 pub risk_level: Option<RiskLevel>,
29}
30
31#[derive(Debug)]
33pub struct CreationResult {
34 pub document_id: DocumentId,
35 pub document_type: DocumentType,
36 pub file_path: PathBuf,
37 pub short_code: String,
38}
39
40impl DocumentCreationService {
41 pub fn new<P: AsRef<Path>>(workspace_dir: P) -> Self {
43 let workspace_path = workspace_dir.as_ref().to_path_buf();
44 let db_path = workspace_path.join("metis.db");
45 Self {
46 workspace_dir: workspace_path,
47 db_path,
48 }
49 }
50
51 fn generate_short_code(&self, doc_type: &str) -> Result<String> {
53 let mut config_repo = ConfigurationRepository::new(
54 SqliteConnection::establish(&self.db_path.to_string_lossy()).map_err(|e| {
55 MetisError::ConfigurationError(
56 crate::domain::configuration::ConfigurationError::InvalidValue(e.to_string()),
57 )
58 })?,
59 );
60
61 config_repo.generate_short_code(doc_type)
62 }
63
64 pub async fn create_vision(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
66 let file_path = self.workspace_dir.join("vision.md");
68
69 if file_path.exists() {
71 return Err(MetisError::ValidationFailed {
72 message: "Vision document already exists".to_string(),
73 });
74 }
75
76 let short_code = self.generate_short_code("vision")?;
78
79 let mut tags = vec![
81 Tag::Label("vision".to_string()),
82 Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
83 ];
84 tags.extend(config.tags);
85
86 let vision = Vision::new(
87 config.title.clone(),
88 tags,
89 false, short_code.clone(),
91 )
92 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
93
94 if let Some(parent) = file_path.parent() {
96 fs::create_dir_all(parent).map_err(|e| MetisError::FileSystem(e.to_string()))?;
97 }
98
99 vision
101 .to_file(&file_path)
102 .await
103 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
104
105 Ok(CreationResult {
106 document_id: vision.id(),
107 document_type: DocumentType::Vision,
108 file_path,
109 short_code,
110 })
111 }
112
113 pub async fn create_strategy(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
115 let short_code = self.generate_short_code("strategy")?;
117 let strategy_dir = self.workspace_dir.join("strategies").join(&short_code);
118 let file_path = strategy_dir.join("strategy.md");
119
120 if file_path.exists() {
122 return Err(MetisError::ValidationFailed {
123 message: format!("Strategy with short code '{}' already exists", short_code),
124 });
125 }
126
127 let mut tags = vec![
129 Tag::Label("strategy".to_string()),
130 Tag::Phase(config.phase.unwrap_or(Phase::Shaping)),
131 ];
132 tags.extend(config.tags);
133
134 let strategy = Strategy::new(
135 config.title.clone(),
136 config.parent_id,
137 Vec::new(), tags,
139 false, config.risk_level.unwrap_or(RiskLevel::Medium), Vec::new(), short_code.clone(),
143 )
144 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
145
146 fs::create_dir_all(&strategy_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
148
149 strategy
151 .to_file(&file_path)
152 .await
153 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
154
155 Ok(CreationResult {
156 document_id: strategy.id(),
157 document_type: DocumentType::Strategy,
158 file_path,
159 short_code,
160 })
161 }
162
163 pub async fn create_initiative(
165 &self,
166 config: DocumentCreationConfig,
167 strategy_id: &str,
168 ) -> Result<CreationResult> {
169 self.create_initiative_with_config(config, strategy_id, &FlightLevelConfig::full())
171 .await
172 }
173
174 pub async fn create_initiative_with_config(
176 &self,
177 config: DocumentCreationConfig,
178 strategy_id: &str,
179 flight_config: &FlightLevelConfig,
180 ) -> Result<CreationResult> {
181 if !flight_config.initiatives_enabled {
183 let enabled_types: Vec<String> = flight_config
184 .enabled_document_types()
185 .iter()
186 .map(|t| t.to_string())
187 .collect();
188 return Err(MetisError::ValidationFailed {
189 message: format!(
190 "Initiative creation is disabled in current configuration ({} mode). Available document types: {}. To enable initiatives, use 'metis config set --preset full' or 'metis config set --initiatives true'",
191 flight_config.preset_name(),
192 enabled_types.join(", ")
193 ),
194 });
195 }
196
197 let short_code = self.generate_short_code("initiative")?;
199
200 let strategy_short_code = if flight_config.strategies_enabled && strategy_id != "NULL" {
202 let db_path = self.workspace_dir.join("metis.db");
204 let db = Database::new(db_path.to_str().unwrap())
205 .map_err(|e| MetisError::FileSystem(format!("Database error: {}", e)))?;
206 let mut repo = db
207 .repository()
208 .map_err(|e| MetisError::FileSystem(format!("Repository error: {}", e)))?;
209
210 let strategy = repo
212 .find_by_short_code(strategy_id)
213 .map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
214 .ok_or_else(|| {
215 MetisError::NotFound(format!("Parent strategy '{}' not found", strategy_id))
216 })?;
217
218 let strategy_file = self
220 .workspace_dir
221 .join("strategies")
222 .join(&strategy.short_code)
223 .join("strategy.md");
224 if !strategy_file.exists() {
225 return Err(MetisError::NotFound(format!(
226 "Parent strategy '{}' not found at expected path",
227 strategy_id
228 )));
229 }
230
231 strategy.short_code
232 } else {
233 "NULL".to_string()
234 };
235
236 let (parent_ref, effective_strategy_id) = if flight_config.strategies_enabled {
238 if strategy_id == "NULL" {
240 return Err(MetisError::ValidationFailed {
241 message: format!(
242 "Cannot create initiative with NULL strategy when strategies are enabled in {} configuration. Provide a valid strategy_id",
243 flight_config.preset_name()
244 ),
245 });
246 }
247
248 (
249 ParentReference::Some(DocumentId::from(strategy_id)),
250 strategy_short_code.as_str(),
251 )
252 } else {
253 (ParentReference::Null, "NULL")
255 };
256
257 let initiative_dir = self
259 .workspace_dir
260 .join("strategies")
261 .join(effective_strategy_id)
262 .join("initiatives")
263 .join(&short_code);
264
265 let file_path = initiative_dir.join("initiative.md");
266
267 if file_path.exists() {
269 return Err(MetisError::ValidationFailed {
270 message: format!("Initiative with short code '{}' already exists", short_code),
271 });
272 }
273
274 let mut tags = vec![
276 Tag::Label("initiative".to_string()),
277 Tag::Phase(config.phase.unwrap_or(Phase::Discovery)),
278 ];
279 tags.extend(config.tags);
280
281 let parent_id = config
283 .parent_id
284 .map(ParentReference::Some)
285 .unwrap_or(parent_ref);
286
287 let initiative = Initiative::new(
288 config.title.clone(),
289 parent_id.parent_id().cloned(), Some(DocumentId::from(effective_strategy_id)), Vec::new(), tags,
293 false, config.complexity.unwrap_or(Complexity::M), short_code.clone(),
296 )
297 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
298
299 fs::create_dir_all(&initiative_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
301
302 initiative
304 .to_file(&file_path)
305 .await
306 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
307
308 Ok(CreationResult {
309 document_id: initiative.id(),
310 document_type: DocumentType::Initiative,
311 file_path,
312 short_code,
313 })
314 }
315
316 pub async fn create_task(
318 &self,
319 config: DocumentCreationConfig,
320 strategy_id: &str,
321 initiative_id: &str,
322 ) -> Result<CreationResult> {
323 self.create_task_with_config(
325 config,
326 strategy_id,
327 initiative_id,
328 &FlightLevelConfig::full(),
329 )
330 .await
331 }
332
333 pub async fn create_task_with_config(
335 &self,
336 config: DocumentCreationConfig,
337 strategy_id: &str,
338 initiative_id: &str,
339 flight_config: &FlightLevelConfig,
340 ) -> Result<CreationResult> {
341 let short_code = self.generate_short_code("task")?;
343
344 let (strategy_short_code, initiative_short_code) = if flight_config.initiatives_enabled
346 && initiative_id != "NULL"
347 {
348 let db_path = self.workspace_dir.join("metis.db");
350 let db = Database::new(db_path.to_str().unwrap())
351 .map_err(|e| MetisError::FileSystem(format!("Database error: {}", e)))?;
352 let mut repo = db
353 .repository()
354 .map_err(|e| MetisError::FileSystem(format!("Repository error: {}", e)))?;
355
356 let initiative = repo
358 .find_by_short_code(initiative_id)
359 .map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
360 .ok_or_else(|| {
361 MetisError::NotFound(format!("Parent initiative '{}' not found", initiative_id))
362 })?;
363
364 let strategy_short_code = if flight_config.strategies_enabled && strategy_id != "NULL" {
366 let strategy = repo
367 .find_by_short_code(strategy_id)
368 .map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
369 .ok_or_else(|| {
370 MetisError::NotFound(format!("Parent strategy '{}' not found", strategy_id))
371 })?;
372 strategy.short_code
373 } else {
374 "NULL".to_string()
375 };
376
377 let initiative_file = self
379 .workspace_dir
380 .join("strategies")
381 .join(&strategy_short_code)
382 .join("initiatives")
383 .join(&initiative.short_code)
384 .join("initiative.md");
385
386 if !initiative_file.exists() {
387 return Err(MetisError::NotFound(format!(
388 "Parent initiative '{}' not found at expected path",
389 initiative_id
390 )));
391 }
392
393 (strategy_short_code, initiative.short_code)
394 } else {
395 ("NULL".to_string(), "NULL".to_string())
396 };
397
398 let (parent_ref, parent_title, effective_strategy_id, effective_initiative_id) =
400 if flight_config.initiatives_enabled {
401 if initiative_id == "NULL" {
403 return Err(MetisError::ValidationFailed {
404 message: format!(
405 "Cannot create task with NULL initiative when initiatives are enabled in {} configuration. Provide a valid initiative_id or create the task as a backlog item",
406 flight_config.preset_name()
407 ),
408 });
409 }
410
411 if flight_config.strategies_enabled && strategy_id == "NULL" {
413 return Err(MetisError::ValidationFailed {
414 message: format!(
415 "Cannot create task with NULL strategy when strategies are enabled in {} configuration. Provide a valid strategy_id or create the task as a backlog item",
416 flight_config.preset_name()
417 ),
418 });
419 }
420
421 (
422 ParentReference::Some(DocumentId::from(initiative_id)),
423 Some(initiative_id.to_string()),
424 strategy_short_code.as_str(),
425 initiative_short_code.as_str(),
426 )
427 } else {
428 (ParentReference::Null, None, "NULL", "NULL")
430 };
431
432 let task_dir = self
434 .workspace_dir
435 .join("strategies")
436 .join(effective_strategy_id)
437 .join("initiatives")
438 .join(effective_initiative_id)
439 .join("tasks");
440
441 let file_path = task_dir.join(format!("{}.md", short_code));
442
443 if file_path.exists() {
445 return Err(MetisError::ValidationFailed {
446 message: format!("Task with short code '{}' already exists", short_code),
447 });
448 }
449
450 let mut tags = vec![
452 Tag::Label("task".to_string()),
453 Tag::Phase(config.phase.unwrap_or(Phase::Todo)),
454 ];
455 tags.extend(config.tags);
456
457 let parent_id = config
459 .parent_id
460 .map(ParentReference::Some)
461 .unwrap_or(parent_ref);
462
463 let task = Task::new(
464 config.title.clone(),
465 parent_id.parent_id().cloned(), parent_title, if effective_strategy_id == "NULL" {
468 None
469 } else {
470 Some(DocumentId::from(effective_strategy_id))
471 },
472 if effective_initiative_id == "NULL" {
473 None
474 } else {
475 Some(DocumentId::from(effective_initiative_id))
476 },
477 Vec::new(), tags,
479 false, short_code.clone(),
481 )
482 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
483
484 if !task_dir.exists() {
486 fs::create_dir_all(&task_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
487 }
488
489 task.to_file(&file_path)
491 .await
492 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
493
494 Ok(CreationResult {
495 document_id: task.id(),
496 document_type: DocumentType::Task,
497 file_path,
498 short_code,
499 })
500 }
501
502 pub async fn create_backlog_item(
504 &self,
505 config: DocumentCreationConfig,
506 ) -> Result<CreationResult> {
507 let short_code = self.generate_short_code("task")?;
509
510 let backlog_dir = self.determine_backlog_directory(&config.tags);
512 let file_path = backlog_dir.join(format!("{}.md", short_code));
513
514 if file_path.exists() {
516 return Err(MetisError::ValidationFailed {
517 message: format!(
518 "Backlog item with short code '{}' already exists",
519 short_code
520 ),
521 });
522 }
523
524 let mut tags = vec![
526 Tag::Label("task".to_string()),
527 Tag::Phase(config.phase.unwrap_or(Phase::Backlog)),
528 ];
529 tags.extend(config.tags);
530
531 let task = Task::new(
532 config.title.clone(),
533 None, None, None, None, Vec::new(), tags,
539 false, short_code.clone(),
541 )
542 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
543
544 if !backlog_dir.exists() {
546 fs::create_dir_all(&backlog_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
547 }
548
549 task.to_file(&file_path)
551 .await
552 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
553
554 Ok(CreationResult {
555 document_id: task.id(),
556 document_type: DocumentType::Task,
557 file_path,
558 short_code,
559 })
560 }
561
562 fn determine_backlog_directory(&self, tags: &[Tag]) -> PathBuf {
564 let base_backlog_dir = self.workspace_dir.join("backlog");
565
566 for tag in tags {
568 if let Tag::Label(label) = tag {
569 match label.as_str() {
570 "bug" => return base_backlog_dir.join("bugs"),
571 "feature" => return base_backlog_dir.join("features"),
572 "tech-debt" => return base_backlog_dir.join("tech-debt"),
573 _ => {}
574 }
575 }
576 }
577
578 base_backlog_dir
580 }
581
582 pub async fn create_adr(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
584 let short_code = self.generate_short_code("adr")?;
586 let adr_filename = format!("{}.md", short_code);
587 let adrs_dir = self.workspace_dir.join("adrs");
588 let file_path = adrs_dir.join(&adr_filename);
589
590 if file_path.exists() {
592 return Err(MetisError::ValidationFailed {
593 message: format!("ADR with short code '{}' already exists", short_code),
594 });
595 }
596
597 let adr_number = self.get_next_adr_number()?;
599
600 let mut tags = vec![
602 Tag::Label("adr".to_string()),
603 Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
604 ];
605 tags.extend(config.tags);
606
607 let adr = Adr::new(
608 adr_number,
609 config.title.clone(),
610 String::new(), None, config.parent_id,
613 tags,
614 false, short_code.clone(),
616 )
617 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
618
619 fs::create_dir_all(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
621
622 adr.to_file(&file_path)
624 .await
625 .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
626
627 Ok(CreationResult {
628 document_id: adr.id(),
629 document_type: DocumentType::Adr,
630 file_path,
631 short_code,
632 })
633 }
634
635 fn get_next_adr_number(&self) -> Result<u32> {
637 let adrs_dir = self.workspace_dir.join("adrs");
638
639 if !adrs_dir.exists() {
640 return Ok(1);
641 }
642
643 let mut max_number = 0;
644 for entry in fs::read_dir(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))? {
645 let entry = entry.map_err(|e| MetisError::FileSystem(e.to_string()))?;
646 let filename = entry.file_name().to_string_lossy().to_string();
647
648 if filename.ends_with(".md") {
649 if let Some(number_str) = filename.split('-').next() {
651 if let Ok(number) = number_str.parse::<u32>() {
652 max_number = max_number.max(number);
653 }
654 }
655 }
656 }
657
658 Ok(max_number + 1)
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use tempfile::tempdir;
666
667 #[tokio::test]
668 async fn test_create_vision_document() {
669 let temp_dir = tempdir().unwrap();
670 let workspace_dir = temp_dir.path().join(".metis");
671 fs::create_dir_all(&workspace_dir).unwrap();
672
673 let db_path = workspace_dir.join("metis.db");
675 let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
676
677 let mut config_repo = ConfigurationRepository::new(
679 SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
680 );
681 config_repo.set_project_prefix("TEST").unwrap();
682
683 let service = DocumentCreationService::new(&workspace_dir);
684 let config = DocumentCreationConfig {
685 title: "Test Vision".to_string(),
686 description: Some("A test vision document".to_string()),
687 parent_id: None,
688 tags: vec![],
689 phase: None,
690 complexity: None,
691 risk_level: None,
692 };
693
694 let result = service.create_vision(config).await.unwrap();
695
696 assert_eq!(result.document_type, DocumentType::Vision);
697 assert!(result.file_path.exists());
698
699 let vision = Vision::from_file(&result.file_path).await.unwrap();
701 assert_eq!(vision.title(), "Test Vision");
702 }
703
704 #[tokio::test]
705 async fn test_create_strategy_document() {
706 let temp_dir = tempdir().unwrap();
707 let workspace_dir = temp_dir.path().join(".metis");
708 fs::create_dir_all(&workspace_dir).unwrap();
709
710 let db_path = workspace_dir.join("metis.db");
712 let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
713
714 let mut config_repo = ConfigurationRepository::new(
716 SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
717 );
718 config_repo.set_project_prefix("TEST").unwrap();
719
720 let service = DocumentCreationService::new(&workspace_dir);
721 let config = DocumentCreationConfig {
722 title: "Test Strategy".to_string(),
723 description: Some("A test strategy document".to_string()),
724 parent_id: None,
725 tags: vec![],
726 phase: None,
727 complexity: None,
728 risk_level: None,
729 };
730
731 let result = service.create_strategy(config).await.unwrap();
732
733 assert_eq!(result.document_type, DocumentType::Strategy);
734 assert!(result.file_path.exists());
735
736 let strategy = Strategy::from_file(&result.file_path).await.unwrap();
738 assert_eq!(strategy.title(), "Test Strategy");
739 }
740
741 #[tokio::test]
742 async fn test_create_initiative_document() {
743 let temp_dir = tempdir().unwrap();
744 let workspace_dir = temp_dir.path().join(".metis");
745 fs::create_dir_all(&workspace_dir).unwrap();
746
747 let db_path = workspace_dir.join("metis.db");
749 let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
750
751 let mut config_repo = ConfigurationRepository::new(
753 SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
754 );
755 config_repo.set_project_prefix("TEST").unwrap();
756
757 let service = DocumentCreationService::new(&workspace_dir);
758
759 let strategy_config = DocumentCreationConfig {
761 title: "Parent Strategy".to_string(),
762 description: Some("A parent strategy".to_string()),
763 parent_id: None,
764 tags: vec![],
765 phase: None,
766 complexity: None,
767 risk_level: None,
768 };
769 let strategy_result = service.create_strategy(strategy_config).await.unwrap();
770 let strategy_id = strategy_result.short_code.clone();
771
772 let db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
774 let mut db_service =
775 crate::application::services::DatabaseService::new(db.repository().unwrap());
776 let mut sync_service = crate::application::services::SyncService::new(&mut db_service);
777 sync_service
778 .import_from_file(&strategy_result.file_path)
779 .await
780 .unwrap();
781
782 let initiative_config = DocumentCreationConfig {
784 title: "Test Initiative".to_string(),
785 description: Some("A test initiative document".to_string()),
786 parent_id: Some(strategy_result.document_id),
787 tags: vec![],
788 phase: None,
789 complexity: None,
790 risk_level: None,
791 };
792
793 let result = service
794 .create_initiative(initiative_config, &strategy_id)
795 .await
796 .unwrap();
797
798 assert_eq!(result.document_type, DocumentType::Initiative);
799 assert!(result.file_path.exists());
800
801 let initiative = Initiative::from_file(&result.file_path).await.unwrap();
803 assert_eq!(initiative.title(), "Test Initiative");
804 }
805
806
807 #[tokio::test]
808 async fn test_get_next_adr_number() {
809 let temp_dir = tempdir().unwrap();
810 let workspace_dir = temp_dir.path().join(".metis");
811 let adrs_dir = workspace_dir.join("adrs");
812 fs::create_dir_all(&adrs_dir).unwrap();
813
814 let service = DocumentCreationService::new(&workspace_dir);
815
816 assert_eq!(service.get_next_adr_number().unwrap(), 1);
818
819 fs::write(adrs_dir.join("001-first-adr.md"), "content").unwrap();
821 fs::write(adrs_dir.join("002-second-adr.md"), "content").unwrap();
822
823 assert_eq!(service.get_next_adr_number().unwrap(), 3);
825 }
826
827 fn setup_test_service_temp() -> (DocumentCreationService, tempfile::TempDir) {
830 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
831 let workspace_dir = temp_dir.path().join(".metis");
832 fs::create_dir_all(&workspace_dir).unwrap();
833
834 let db_path = workspace_dir.join("metis.db");
836 let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
837
838 let mut config_repo = ConfigurationRepository::new(
840 SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
841 );
842 config_repo.set_project_prefix("TEST").unwrap();
843
844 let service = DocumentCreationService::new(&workspace_dir);
845 (service, temp_dir)
846 }
847
848 #[tokio::test]
849 async fn test_create_initiative_full_configuration() {
850 let (service, temp) = setup_test_service_temp();
851 let flight_config = FlightLevelConfig::full();
852
853 let strategy_config = DocumentCreationConfig {
855 title: "Test Strategy".to_string(),
856 description: None,
857 parent_id: None,
858 tags: vec![],
859 phase: None,
860 complexity: None,
861 risk_level: None,
862 };
863
864 let strategy_result = service.create_strategy(strategy_config).await.unwrap();
865
866 let db_path = temp.path().join(".metis/metis.db");
868 let db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
869 let mut db_service =
870 crate::application::services::DatabaseService::new(db.repository().unwrap());
871 let mut sync_service = crate::application::services::SyncService::new(&mut db_service);
872 sync_service
873 .import_from_file(&strategy_result.file_path)
874 .await
875 .unwrap();
876
877 let initiative_config = DocumentCreationConfig {
879 title: "Test Initiative".to_string(),
880 description: None,
881 parent_id: None,
882 tags: vec![],
883 phase: None,
884 complexity: None,
885 risk_level: None,
886 };
887
888 let result = service
889 .create_initiative_with_config(
890 initiative_config,
891 &strategy_result.short_code,
892 &flight_config,
893 )
894 .await
895 .unwrap();
896
897 assert_eq!(result.document_type, DocumentType::Initiative);
898 assert!(result.file_path.exists());
899
900 assert!(result.file_path.to_string_lossy().contains("strategies"));
902 assert!(result.file_path.to_string_lossy().contains("initiatives"));
903 }
904
905 #[tokio::test]
906 async fn test_create_initiative_streamlined_configuration() {
907 let (service, _temp) = setup_test_service_temp();
908 let flight_config = FlightLevelConfig::streamlined();
909
910 let initiative_config = DocumentCreationConfig {
911 title: "Test Initiative".to_string(),
912 description: None,
913 parent_id: None,
914 tags: vec![],
915 phase: None,
916 complexity: None,
917 risk_level: None,
918 };
919
920 let result = service
922 .create_initiative_with_config(initiative_config, "NULL", &flight_config)
923 .await
924 .unwrap();
925
926 assert_eq!(result.document_type, DocumentType::Initiative);
927 assert!(result.file_path.exists());
928
929 assert!(result.file_path.to_string_lossy().contains("initiatives"));
931 assert!(result
932 .file_path
933 .to_string_lossy()
934 .contains("strategies/NULL"));
935 }
936
937 #[tokio::test]
938 async fn test_create_initiative_disabled_in_direct_configuration() {
939 let (service, _temp) = setup_test_service_temp();
940 let flight_config = FlightLevelConfig::direct();
941
942 let initiative_config = DocumentCreationConfig {
943 title: "Test Initiative".to_string(),
944 description: None,
945 parent_id: None,
946 tags: vec![],
947 phase: None,
948 complexity: None,
949 risk_level: None,
950 };
951
952 let result = service
954 .create_initiative_with_config(initiative_config, "NULL", &flight_config)
955 .await;
956
957 assert!(result.is_err());
958 assert!(result
959 .unwrap_err()
960 .to_string()
961 .contains("Initiative creation is disabled"));
962 }
963
964 #[tokio::test]
965 async fn test_create_task_direct_configuration() {
966 let (service, _temp) = setup_test_service_temp();
967 let flight_config = FlightLevelConfig::direct();
968
969 let task_config = DocumentCreationConfig {
970 title: "Test Task".to_string(),
971 description: None,
972 parent_id: None,
973 tags: vec![],
974 phase: None,
975 complexity: None,
976 risk_level: None,
977 };
978
979 let result = service
981 .create_task_with_config(task_config, "NULL", "NULL", &flight_config)
982 .await
983 .unwrap();
984
985 assert_eq!(result.document_type, DocumentType::Task);
986 assert!(result.file_path.exists());
987
988 assert!(result.file_path.to_string_lossy().contains("tasks"));
990 assert!(result
991 .file_path
992 .to_string_lossy()
993 .contains("strategies/NULL/initiatives/NULL"));
994 }
995}