use crate::application::services::template::TemplateLoader;
use crate::dal::database::configuration_repository::ConfigurationRepository;
use crate::domain::configuration::FlightLevelConfig;
use crate::domain::documents::initiative::Complexity;
use crate::domain::documents::strategy::RiskLevel;
use crate::domain::documents::traits::Document;
use crate::domain::documents::types::{DocumentId, DocumentType, ParentReference, Phase, Tag};
use crate::Result;
use crate::{Adr, Database, Initiative, MetisError, Strategy, Task, Vision};
use diesel::{sqlite::SqliteConnection, Connection};
use std::fs;
use std::path::{Path, PathBuf};
pub struct DocumentCreationService {
workspace_dir: PathBuf,
db_path: PathBuf,
template_loader: TemplateLoader,
}
#[derive(Debug, Clone)]
pub struct DocumentCreationConfig {
pub title: String,
pub description: Option<String>,
pub parent_id: Option<DocumentId>,
pub tags: Vec<Tag>,
pub phase: Option<Phase>,
pub complexity: Option<Complexity>,
pub risk_level: Option<RiskLevel>,
}
#[derive(Debug)]
pub struct CreationResult {
pub document_id: DocumentId,
pub document_type: DocumentType,
pub file_path: PathBuf,
pub short_code: String,
}
impl DocumentCreationService {
pub fn new<P: AsRef<Path>>(workspace_dir: P) -> Self {
let workspace_path = workspace_dir.as_ref().to_path_buf();
let db_path = workspace_path.join("metis.db");
let template_loader = TemplateLoader::for_workspace(&workspace_path);
Self {
workspace_dir: workspace_path,
db_path,
template_loader,
}
}
fn generate_short_code(&self, doc_type: &str) -> Result<String> {
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&self.db_path.to_string_lossy()).map_err(|e| {
MetisError::ConfigurationError(
crate::domain::configuration::ConfigurationError::InvalidValue(e.to_string()),
)
})?,
);
config_repo.generate_short_code(doc_type)
}
pub async fn create_vision(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
let file_path = self.workspace_dir.join("vision.md");
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: "Vision document already exists".to_string(),
});
}
let short_code = self.generate_short_code("vision")?;
let template_content = self
.template_loader
.load_content_template("vision")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("vision".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
];
tags.extend(config.tags);
let vision = Vision::new_with_template(
config.title.clone(),
tags,
false, short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).map_err(|e| MetisError::FileSystem(e.to_string()))?;
}
vision
.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: vision.id(),
document_type: DocumentType::Vision,
file_path,
short_code,
})
}
pub async fn create_strategy(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
let short_code = self.generate_short_code("strategy")?;
let strategy_dir = self.workspace_dir.join("strategies").join(&short_code);
let file_path = strategy_dir.join("strategy.md");
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: format!("Strategy with short code '{}' already exists", short_code),
});
}
let template_content = self
.template_loader
.load_content_template("strategy")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("strategy".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Shaping)),
];
tags.extend(config.tags);
let strategy = Strategy::new_with_template(
config.title.clone(),
config.parent_id,
Vec::new(), tags,
false, config.risk_level.unwrap_or(RiskLevel::Medium), Vec::new(), short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
fs::create_dir_all(&strategy_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
strategy
.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: strategy.id(),
document_type: DocumentType::Strategy,
file_path,
short_code,
})
}
pub async fn create_initiative(
&self,
config: DocumentCreationConfig,
strategy_id: &str,
) -> Result<CreationResult> {
self.create_initiative_with_config(config, strategy_id, &FlightLevelConfig::full())
.await
}
pub async fn create_initiative_with_config(
&self,
config: DocumentCreationConfig,
strategy_id: &str,
flight_config: &FlightLevelConfig,
) -> Result<CreationResult> {
if !flight_config.initiatives_enabled {
let enabled_types: Vec<String> = flight_config
.enabled_document_types()
.iter()
.map(|t| t.to_string())
.collect();
return Err(MetisError::ValidationFailed {
message: format!(
"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'",
flight_config.preset_name(),
enabled_types.join(", ")
),
});
}
let short_code = self.generate_short_code("initiative")?;
let strategy_short_code = if flight_config.strategies_enabled && strategy_id != "NULL" {
let db_path = self.workspace_dir.join("metis.db");
let db = Database::new(db_path.to_str().unwrap())
.map_err(|e| MetisError::FileSystem(format!("Database error: {}", e)))?;
let mut repo = db
.repository()
.map_err(|e| MetisError::FileSystem(format!("Repository error: {}", e)))?;
let strategy = repo
.find_by_short_code(strategy_id)
.map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
.ok_or_else(|| {
MetisError::NotFound(format!("Parent strategy '{}' not found", strategy_id))
})?;
let strategy_file = self
.workspace_dir
.join("strategies")
.join(&strategy.short_code)
.join("strategy.md");
if !strategy_file.exists() {
return Err(MetisError::NotFound(format!(
"Parent strategy '{}' not found at expected path",
strategy_id
)));
}
strategy.short_code
} else {
"NULL".to_string()
};
let (parent_ref, effective_strategy_id) = if flight_config.strategies_enabled {
if strategy_id == "NULL" {
return Err(MetisError::ValidationFailed {
message: format!(
"Cannot create initiative with NULL strategy when strategies are enabled in {} configuration. Provide a valid strategy_id",
flight_config.preset_name()
),
});
}
(
ParentReference::Some(DocumentId::from(strategy_id)),
strategy_short_code.as_str(),
)
} else {
(ParentReference::Null, "NULL")
};
let initiative_dir = self
.workspace_dir
.join("strategies")
.join(effective_strategy_id)
.join("initiatives")
.join(&short_code);
let file_path = initiative_dir.join("initiative.md");
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: format!("Initiative with short code '{}' already exists", short_code),
});
}
let template_content = self
.template_loader
.load_content_template("initiative")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("initiative".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Discovery)),
];
tags.extend(config.tags);
let parent_id = config
.parent_id
.map(ParentReference::Some)
.unwrap_or(parent_ref);
let initiative = Initiative::new_with_template(
config.title.clone(),
parent_id.parent_id().cloned(), Some(DocumentId::from(effective_strategy_id)), Vec::new(), tags,
false, config.complexity.unwrap_or(Complexity::M), short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
fs::create_dir_all(&initiative_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
initiative
.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: initiative.id(),
document_type: DocumentType::Initiative,
file_path,
short_code,
})
}
pub async fn create_task(
&self,
config: DocumentCreationConfig,
strategy_id: &str,
initiative_id: &str,
) -> Result<CreationResult> {
self.create_task_with_config(
config,
strategy_id,
initiative_id,
&FlightLevelConfig::full(),
)
.await
}
pub async fn create_task_with_config(
&self,
config: DocumentCreationConfig,
strategy_id: &str,
initiative_id: &str,
flight_config: &FlightLevelConfig,
) -> Result<CreationResult> {
let short_code = self.generate_short_code("task")?;
let (strategy_short_code, initiative_short_code) = if flight_config.initiatives_enabled
&& initiative_id != "NULL"
{
let db_path = self.workspace_dir.join("metis.db");
let db = Database::new(db_path.to_str().unwrap())
.map_err(|e| MetisError::FileSystem(format!("Database error: {}", e)))?;
let mut repo = db
.repository()
.map_err(|e| MetisError::FileSystem(format!("Repository error: {}", e)))?;
let initiative = repo
.find_by_short_code(initiative_id)
.map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
.ok_or_else(|| {
MetisError::NotFound(format!("Parent initiative '{}' not found", initiative_id))
})?;
let strategy_short_code = if flight_config.strategies_enabled && strategy_id != "NULL" {
let strategy = repo
.find_by_short_code(strategy_id)
.map_err(|e| MetisError::FileSystem(format!("Database lookup error: {}", e)))?
.ok_or_else(|| {
MetisError::NotFound(format!("Parent strategy '{}' not found", strategy_id))
})?;
strategy.short_code
} else {
"NULL".to_string()
};
let initiative_file = self
.workspace_dir
.join("strategies")
.join(&strategy_short_code)
.join("initiatives")
.join(&initiative.short_code)
.join("initiative.md");
if !initiative_file.exists() {
return Err(MetisError::NotFound(format!(
"Parent initiative '{}' not found at expected path",
initiative_id
)));
}
(strategy_short_code, initiative.short_code)
} else {
("NULL".to_string(), "NULL".to_string())
};
let (parent_ref, parent_title, effective_strategy_id, effective_initiative_id) =
if flight_config.initiatives_enabled {
if initiative_id == "NULL" {
return Err(MetisError::ValidationFailed {
message: format!(
"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",
flight_config.preset_name()
),
});
}
if flight_config.strategies_enabled && strategy_id == "NULL" {
return Err(MetisError::ValidationFailed {
message: format!(
"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",
flight_config.preset_name()
),
});
}
(
ParentReference::Some(DocumentId::from(initiative_id)),
Some(initiative_id.to_string()),
strategy_short_code.as_str(),
initiative_short_code.as_str(),
)
} else {
(ParentReference::Null, None, "NULL", "NULL")
};
let task_dir = self
.workspace_dir
.join("strategies")
.join(effective_strategy_id)
.join("initiatives")
.join(effective_initiative_id)
.join("tasks");
let file_path = task_dir.join(format!("{}.md", short_code));
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: format!("Task with short code '{}' already exists", short_code),
});
}
let template_content = self
.template_loader
.load_content_template("task")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("task".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Todo)),
];
tags.extend(config.tags);
let parent_id = config
.parent_id
.map(ParentReference::Some)
.unwrap_or(parent_ref);
let task = Task::new_with_template(
config.title.clone(),
parent_id.parent_id().cloned(), parent_title, if effective_strategy_id == "NULL" {
None
} else {
Some(DocumentId::from(effective_strategy_id))
},
if effective_initiative_id == "NULL" {
None
} else {
Some(DocumentId::from(effective_initiative_id))
},
Vec::new(), tags,
false, short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
if !task_dir.exists() {
fs::create_dir_all(&task_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
}
task.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: task.id(),
document_type: DocumentType::Task,
file_path,
short_code,
})
}
pub async fn create_backlog_item(
&self,
config: DocumentCreationConfig,
) -> Result<CreationResult> {
let short_code = self.generate_short_code("task")?;
let backlog_dir = self.determine_backlog_directory(&config.tags);
let file_path = backlog_dir.join(format!("{}.md", short_code));
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: format!(
"Backlog item with short code '{}' already exists",
short_code
),
});
}
let template_content = self
.template_loader
.load_content_template("task")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("task".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Backlog)),
];
tags.extend(config.tags);
let task = Task::new_with_template(
config.title.clone(),
None, None, None, None, Vec::new(), tags,
false, short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
if !backlog_dir.exists() {
fs::create_dir_all(&backlog_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
}
task.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: task.id(),
document_type: DocumentType::Task,
file_path,
short_code,
})
}
fn determine_backlog_directory(&self, tags: &[Tag]) -> PathBuf {
let base_backlog_dir = self.workspace_dir.join("backlog");
for tag in tags {
if let Tag::Label(label) = tag {
match label.as_str() {
"bug" => return base_backlog_dir.join("bugs"),
"feature" => return base_backlog_dir.join("features"),
"tech-debt" => return base_backlog_dir.join("tech-debt"),
_ => {}
}
}
}
base_backlog_dir
}
pub async fn create_adr(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
let short_code = self.generate_short_code("adr")?;
let adr_filename = format!("{}.md", short_code);
let adrs_dir = self.workspace_dir.join("adrs");
let file_path = adrs_dir.join(&adr_filename);
if file_path.exists() {
return Err(MetisError::ValidationFailed {
message: format!("ADR with short code '{}' already exists", short_code),
});
}
let adr_number = self.get_next_adr_number()?;
let template_content = self
.template_loader
.load_content_template("adr")
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
let mut tags = vec![
Tag::Label("adr".to_string()),
Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
];
tags.extend(config.tags);
let adr = Adr::new_with_template(
adr_number,
config.title.clone(),
String::new(), None, config.parent_id,
tags,
false, short_code.clone(),
&template_content,
)
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
fs::create_dir_all(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
adr.to_file(&file_path)
.await
.map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
Ok(CreationResult {
document_id: adr.id(),
document_type: DocumentType::Adr,
file_path,
short_code,
})
}
fn get_next_adr_number(&self) -> Result<u32> {
let adrs_dir = self.workspace_dir.join("adrs");
if !adrs_dir.exists() {
return Ok(1);
}
let mut max_number = 0;
for entry in fs::read_dir(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))? {
let entry = entry.map_err(|e| MetisError::FileSystem(e.to_string()))?;
let filename = entry.file_name().to_string_lossy().to_string();
if filename.ends_with(".md") {
if let Some(number_str) = filename.split('-').next() {
if let Ok(number) = number_str.parse::<u32>() {
max_number = max_number.max(number);
}
}
}
}
Ok(max_number + 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_create_vision_document() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let config = DocumentCreationConfig {
title: "Test Vision".to_string(),
description: Some("A test vision document".to_string()),
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service.create_vision(config).await.unwrap();
assert_eq!(result.document_type, DocumentType::Vision);
assert!(result.file_path.exists());
let vision = Vision::from_file(&result.file_path).await.unwrap();
assert_eq!(vision.title(), "Test Vision");
}
#[tokio::test]
async fn test_create_strategy_document() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let config = DocumentCreationConfig {
title: "Test Strategy".to_string(),
description: Some("A test strategy document".to_string()),
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service.create_strategy(config).await.unwrap();
assert_eq!(result.document_type, DocumentType::Strategy);
assert!(result.file_path.exists());
let strategy = Strategy::from_file(&result.file_path).await.unwrap();
assert_eq!(strategy.title(), "Test Strategy");
}
#[tokio::test]
async fn test_create_initiative_document() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let strategy_config = DocumentCreationConfig {
title: "Parent Strategy".to_string(),
description: Some("A parent strategy".to_string()),
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let strategy_result = service.create_strategy(strategy_config).await.unwrap();
let strategy_id = strategy_result.short_code.clone();
let db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut db_service =
crate::application::services::DatabaseService::new(db.repository().unwrap());
let mut sync_service = crate::application::services::SyncService::new(&mut db_service);
sync_service
.import_from_file(&strategy_result.file_path)
.await
.unwrap();
let initiative_config = DocumentCreationConfig {
title: "Test Initiative".to_string(),
description: Some("A test initiative document".to_string()),
parent_id: Some(strategy_result.document_id),
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_initiative(initiative_config, &strategy_id)
.await
.unwrap();
assert_eq!(result.document_type, DocumentType::Initiative);
assert!(result.file_path.exists());
let initiative = Initiative::from_file(&result.file_path).await.unwrap();
assert_eq!(initiative.title(), "Test Initiative");
}
#[tokio::test]
async fn test_get_next_adr_number() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
let adrs_dir = workspace_dir.join("adrs");
fs::create_dir_all(&adrs_dir).unwrap();
let service = DocumentCreationService::new(&workspace_dir);
assert_eq!(service.get_next_adr_number().unwrap(), 1);
fs::write(adrs_dir.join("001-first-adr.md"), "content").unwrap();
fs::write(adrs_dir.join("002-second-adr.md"), "content").unwrap();
assert_eq!(service.get_next_adr_number().unwrap(), 3);
}
fn setup_test_service_temp() -> (DocumentCreationService, tempfile::TempDir) {
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
(service, temp_dir)
}
#[tokio::test]
async fn test_create_initiative_full_configuration() {
let (service, temp) = setup_test_service_temp();
let flight_config = FlightLevelConfig::full();
let strategy_config = DocumentCreationConfig {
title: "Test Strategy".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let strategy_result = service.create_strategy(strategy_config).await.unwrap();
let db_path = temp.path().join(".metis/metis.db");
let db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut db_service =
crate::application::services::DatabaseService::new(db.repository().unwrap());
let mut sync_service = crate::application::services::SyncService::new(&mut db_service);
sync_service
.import_from_file(&strategy_result.file_path)
.await
.unwrap();
let initiative_config = DocumentCreationConfig {
title: "Test Initiative".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_initiative_with_config(
initiative_config,
&strategy_result.short_code,
&flight_config,
)
.await
.unwrap();
assert_eq!(result.document_type, DocumentType::Initiative);
assert!(result.file_path.exists());
assert!(result.file_path.to_string_lossy().contains("strategies"));
assert!(result.file_path.to_string_lossy().contains("initiatives"));
}
#[tokio::test]
async fn test_create_initiative_streamlined_configuration() {
let (service, _temp) = setup_test_service_temp();
let flight_config = FlightLevelConfig::streamlined();
let initiative_config = DocumentCreationConfig {
title: "Test Initiative".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_initiative_with_config(initiative_config, "NULL", &flight_config)
.await
.unwrap();
assert_eq!(result.document_type, DocumentType::Initiative);
assert!(result.file_path.exists());
assert!(result.file_path.to_string_lossy().contains("initiatives"));
assert!(result
.file_path
.to_string_lossy()
.contains("strategies/NULL"));
}
#[tokio::test]
async fn test_create_initiative_disabled_in_direct_configuration() {
let (service, _temp) = setup_test_service_temp();
let flight_config = FlightLevelConfig::direct();
let initiative_config = DocumentCreationConfig {
title: "Test Initiative".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_initiative_with_config(initiative_config, "NULL", &flight_config)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Initiative creation is disabled"));
}
#[tokio::test]
async fn test_create_task_direct_configuration() {
let (service, _temp) = setup_test_service_temp();
let flight_config = FlightLevelConfig::direct();
let task_config = DocumentCreationConfig {
title: "Test Task".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_task_with_config(task_config, "NULL", "NULL", &flight_config)
.await
.unwrap();
assert_eq!(result.document_type, DocumentType::Task);
assert!(result.file_path.exists());
assert!(result.file_path.to_string_lossy().contains("tasks"));
assert!(result
.file_path
.to_string_lossy()
.contains("strategies/NULL/initiatives/NULL"));
}
#[tokio::test]
async fn test_create_vision_with_custom_template() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let template_dir = workspace_dir.join("templates").join("vision");
fs::create_dir_all(&template_dir).unwrap();
let custom_template = r#"# {{ title }}
## Custom Vision Section
This is a custom template for testing.
## Goals
- Custom goal 1
- Custom goal 2
"#;
fs::write(template_dir.join("content.md"), custom_template).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let config = DocumentCreationConfig {
title: "Custom Vision".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service.create_vision(config).await.unwrap();
let content = fs::read_to_string(&result.file_path).unwrap();
assert!(
content.contains("Custom Vision Section"),
"Should contain custom template section"
);
assert!(
content.contains("Custom goal 1"),
"Should contain custom template content"
);
}
#[tokio::test]
async fn test_create_task_with_custom_template() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let template_dir = workspace_dir.join("templates").join("task");
fs::create_dir_all(&template_dir).unwrap();
let custom_template = r#"# {{ title }}
## Definition of Done
- [ ] Custom criterion 1
- [ ] Custom criterion 2
## Parent: {{ parent_title }}
"#;
fs::write(template_dir.join("content.md"), custom_template).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let flight_config = FlightLevelConfig::direct();
let task_config = DocumentCreationConfig {
title: "Custom Task".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service
.create_task_with_config(task_config, "NULL", "NULL", &flight_config)
.await
.unwrap();
let content = fs::read_to_string(&result.file_path).unwrap();
assert!(
content.contains("Definition of Done"),
"Should contain custom template section"
);
assert!(
content.contains("Custom criterion 1"),
"Should contain custom template content"
);
}
#[tokio::test]
async fn test_create_document_falls_back_to_embedded_template() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let config = DocumentCreationConfig {
title: "Fallback Vision".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service.create_vision(config).await.unwrap();
assert!(result.file_path.exists());
let content = fs::read_to_string(&result.file_path).unwrap();
assert!(
content.contains("Fallback Vision"),
"Should contain the title"
);
}
#[tokio::test]
async fn test_invalid_custom_template_fails_gracefully() {
let temp_dir = tempdir().unwrap();
let workspace_dir = temp_dir.path().join(".metis");
fs::create_dir_all(&workspace_dir).unwrap();
let template_dir = workspace_dir.join("templates").join("vision");
fs::create_dir_all(&template_dir).unwrap();
let invalid_template = r#"# {{ title }
This template has a syntax error (unclosed tag above).
"#;
fs::write(template_dir.join("content.md"), invalid_template).unwrap();
let db_path = workspace_dir.join("metis.db");
let _db = crate::Database::new(&db_path.to_string_lossy()).unwrap();
let mut config_repo = ConfigurationRepository::new(
SqliteConnection::establish(&db_path.to_string_lossy()).unwrap(),
);
config_repo.set_project_prefix("TEST").unwrap();
let service = DocumentCreationService::new(&workspace_dir);
let config = DocumentCreationConfig {
title: "Should Fail".to_string(),
description: None,
parent_id: None,
tags: vec![],
phase: None,
complexity: None,
risk_level: None,
};
let result = service.create_vision(config).await;
assert!(result.is_err(), "Should fail with invalid template");
}
}