metis_core/domain/documents/task/
mod.rs

1use super::content::DocumentContent;
2use super::helpers::FrontmatterParser;
3use super::metadata::DocumentMetadata;
4use super::traits::{Document, DocumentTemplate, DocumentValidationError};
5use super::types::{DocumentId, DocumentType, Phase, Tag};
6use chrono::Utc;
7use gray_matter;
8use std::path::Path;
9use tera::{Context, Tera};
10
11/// A Task document represents a concrete, actionable piece of work
12#[derive(Debug)]
13pub struct Task {
14    core: super::traits::DocumentCore,
15}
16
17impl Task {
18    /// Create a new Task document with content rendered from template
19    pub fn new(
20        title: String,
21        parent_id: Option<DocumentId>, // Usually an Initiative
22        parent_title: Option<String>,  // Title of parent for template rendering
23        strategy_id: Option<DocumentId>, // The strategy this task belongs to
24        initiative_id: Option<DocumentId>, // The initiative this task belongs to
25        blocked_by: Vec<DocumentId>,
26        tags: Vec<Tag>,
27        archived: bool,
28    ) -> Result<Self, DocumentValidationError> {
29        // Create fresh metadata
30        let metadata = DocumentMetadata::new();
31
32        // Render the content template
33        let template_content = include_str!("content.md");
34        let mut tera = Tera::default();
35        tera.add_raw_template("task_content", template_content)
36            .map_err(|e| {
37                DocumentValidationError::InvalidContent(format!("Template error: {}", e))
38            })?;
39
40        let mut context = Context::new();
41        context.insert("title", &title);
42        context.insert(
43            "parent_title",
44            &parent_title.unwrap_or_else(|| "Parent Initiative".to_string()),
45        );
46
47        let rendered_content = tera.render("task_content", &context).map_err(|e| {
48            DocumentValidationError::InvalidContent(format!("Template render error: {}", e))
49        })?;
50
51        let content = DocumentContent::new(&rendered_content);
52
53        Ok(Self {
54            core: super::traits::DocumentCore {
55                title,
56                metadata,
57                content,
58                parent_id,
59                blocked_by,
60                tags,
61                archived,
62                strategy_id,
63                initiative_id,
64            },
65        })
66    }
67
68    /// Create a Task document from existing data (used when loading from file)
69    pub fn from_parts(
70        title: String,
71        metadata: DocumentMetadata,
72        content: DocumentContent,
73        parent_id: Option<DocumentId>,
74        strategy_id: Option<DocumentId>,
75        initiative_id: Option<DocumentId>,
76        blocked_by: Vec<DocumentId>,
77        tags: Vec<Tag>,
78        archived: bool,
79    ) -> Self {
80        Self {
81            core: super::traits::DocumentCore {
82                title,
83                metadata,
84                content,
85                parent_id,
86                blocked_by,
87                tags,
88                archived,
89                strategy_id,
90                initiative_id,
91            },
92        }
93    }
94
95    /// Create a Task document by reading and parsing a file
96    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DocumentValidationError> {
97        let raw_content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
98            DocumentValidationError::InvalidContent(format!("Failed to read file: {}", e))
99        })?;
100
101        Self::from_content(&raw_content)
102    }
103
104    /// Create a Task document from raw file content string
105    pub fn from_content(raw_content: &str) -> Result<Self, DocumentValidationError> {
106        // Parse frontmatter and content
107        let parsed = gray_matter::Matter::<gray_matter::engine::YAML>::new().parse(raw_content);
108
109        // Extract frontmatter data
110        let frontmatter = parsed.data.ok_or_else(|| {
111            DocumentValidationError::MissingRequiredField("frontmatter".to_string())
112        })?;
113
114        // Parse frontmatter into structured data
115        let fm_map = match frontmatter {
116            gray_matter::Pod::Hash(map) => map,
117            _ => {
118                return Err(DocumentValidationError::InvalidContent(
119                    "Frontmatter must be a hash/map".to_string(),
120                ))
121            }
122        };
123
124        // Extract required fields
125        let title = FrontmatterParser::extract_string(&fm_map, "title")?;
126        let archived = FrontmatterParser::extract_bool(&fm_map, "archived").unwrap_or(false);
127
128        // Parse timestamps
129        let created_at = FrontmatterParser::extract_datetime(&fm_map, "created_at")?;
130        let updated_at = FrontmatterParser::extract_datetime(&fm_map, "updated_at")?;
131        let exit_criteria_met =
132            FrontmatterParser::extract_bool(&fm_map, "exit_criteria_met").unwrap_or(false);
133
134        // Parse tags
135        let tags = FrontmatterParser::extract_tags(&fm_map)?;
136
137        // Verify this is actually a task document
138        let level = FrontmatterParser::extract_string(&fm_map, "level")?;
139        if level != "task" {
140            return Err(DocumentValidationError::InvalidContent(format!(
141                "Expected level 'task', found '{}'",
142                level
143            )));
144        }
145
146        // Extract task-specific fields
147        let parent_id = FrontmatterParser::extract_string(&fm_map, "parent")
148            .ok()
149            .map(DocumentId::from);
150        let blocked_by = FrontmatterParser::extract_string_array(&fm_map, "blocked_by")
151            .unwrap_or_default()
152            .into_iter()
153            .map(DocumentId::from)
154            .collect();
155
156        // Create metadata and content
157        let metadata =
158            DocumentMetadata::from_frontmatter(created_at, updated_at, exit_criteria_met);
159        let content = DocumentContent::from_markdown(&parsed.content);
160
161        // Extract lineage from frontmatter
162        let strategy_id = FrontmatterParser::extract_optional_string(&fm_map, "strategy_id")
163            .map(DocumentId::from);
164        let initiative_id = FrontmatterParser::extract_optional_string(&fm_map, "initiative_id")
165            .map(DocumentId::from);
166        
167        Ok(Self::from_parts(
168            title, metadata, content, parent_id, strategy_id, initiative_id, blocked_by, tags, archived,
169        ))
170    }
171
172    /// Get the next phase in the Task sequence
173    fn next_phase_in_sequence(current: Phase) -> Option<Phase> {
174        use Phase::*;
175        match current {
176            Backlog => None,   // Backlog doesn't auto-transition - must be explicitly assigned
177            Todo => Some(Active),
178            Active => Some(Completed),
179            Completed => None, // Final phase
180            Blocked => None,   // Blocked doesn't auto-transition
181            _ => None,         // Invalid phase for Task
182        }
183    }
184
185    /// Update the phase tag in the document's tags
186    fn update_phase_tag(&mut self, new_phase: Phase) {
187        // Remove any existing phase tags
188        self.core.tags.retain(|tag| !matches!(tag, Tag::Phase(_)));
189        // Add the new phase tag
190        self.core.tags.push(Tag::Phase(new_phase));
191        // Update timestamp
192        self.core.metadata.updated_at = Utc::now();
193    }
194
195    /// Write the Task document to a file
196    pub async fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DocumentValidationError> {
197        let content = self.to_content()?;
198        std::fs::write(path.as_ref(), content).map_err(|e| {
199            DocumentValidationError::InvalidContent(format!("Failed to write file: {}", e))
200        })
201    }
202
203    /// Convert the Task document to its markdown string representation using templates
204    pub fn to_content(&self) -> Result<String, DocumentValidationError> {
205        let mut tera = Tera::default();
206
207        // Add the frontmatter template to Tera
208        tera.add_raw_template("frontmatter", self.frontmatter_template())
209            .map_err(|e| {
210                DocumentValidationError::InvalidContent(format!("Template error: {}", e))
211            })?;
212
213        // Create context with all document data
214        let mut context = Context::new();
215        context.insert("slug", &self.id().to_string());
216        context.insert("title", self.title());
217        context.insert("created_at", &self.metadata().created_at.to_rfc3339());
218        context.insert("updated_at", &self.metadata().updated_at.to_rfc3339());
219        context.insert("archived", &self.archived().to_string());
220        context.insert(
221            "exit_criteria_met",
222            &self.metadata().exit_criteria_met.to_string(),
223        );
224        context.insert(
225            "parent_id",
226            &self
227                .parent_id()
228                .map(|id| id.to_string())
229                .unwrap_or_default(),
230        );
231        let blocked_by_list: Vec<String> =
232            self.blocked_by().iter().map(|id| id.to_string()).collect();
233        context.insert("blocked_by", &blocked_by_list);
234
235        // Convert tags to strings
236        let tag_strings: Vec<String> = self.tags().iter().map(|tag| tag.to_str()).collect();
237        context.insert("tags", &tag_strings);
238        
239        // Add lineage fields
240        context.insert(
241            "strategy_id",
242            &self.core.strategy_id
243                .as_ref()
244                .map(|id| id.to_string())
245                .unwrap_or_default(),
246        );
247        context.insert(
248            "initiative_id",
249            &self.core.initiative_id
250                .as_ref()
251                .map(|id| id.to_string())
252                .unwrap_or_default(),
253        );
254
255        // Render frontmatter
256        let frontmatter = tera.render("frontmatter", &context).map_err(|e| {
257            DocumentValidationError::InvalidContent(format!("Frontmatter render error: {}", e))
258        })?;
259
260        // Use the actual content body
261        let content_body = &self.content().body;
262
263        // Use actual acceptance criteria if present, otherwise empty string
264        let acceptance_criteria = if let Some(ac) = &self.content().acceptance_criteria {
265            format!("\n\n## Acceptance Criteria\n\n{}", ac)
266        } else {
267            String::new()
268        };
269
270        // Combine everything
271        Ok(format!(
272            "---\n{}\n---\n\n{}{}",
273            frontmatter.trim_end(),
274            content_body,
275            acceptance_criteria
276        ))
277    }
278}
279
280impl Document for Task {
281    // id() uses default implementation from trait
282
283    fn document_type(&self) -> DocumentType {
284        DocumentType::Task
285    }
286
287    fn title(&self) -> &str {
288        &self.core.title
289    }
290
291    fn metadata(&self) -> &DocumentMetadata {
292        &self.core.metadata
293    }
294
295    fn content(&self) -> &DocumentContent {
296        &self.core.content
297    }
298
299    fn core(&self) -> &super::traits::DocumentCore {
300        &self.core
301    }
302
303    fn can_transition_to(&self, phase: Phase) -> bool {
304        if let Ok(current_phase) = self.phase() {
305            use Phase::*;
306            match (current_phase, phase) {
307                (Backlog, Todo) => true, // Move from backlog to todo when assigned to initiative
308                (Todo, Active) => true,
309                (Active, Completed) => true,
310                (Active, Blocked) => true,
311                (Todo, Blocked) => true, // Can pre emptively be blocked while in backlog
312                (Blocked, Active) => true,
313                (Blocked, Todo) => true, // Can go back to todo if unblocked
314                _ => false,
315            }
316        } else {
317            false // Can't transition if we can't determine current phase
318        }
319    }
320
321    fn parent_id(&self) -> Option<&DocumentId> {
322        self.core.parent_id.as_ref()
323    }
324
325    fn blocked_by(&self) -> &[DocumentId] {
326        &self.core.blocked_by
327    }
328
329    fn validate(&self) -> Result<(), DocumentValidationError> {
330        // Task-specific validation rules
331        if self.title().trim().is_empty() {
332            return Err(DocumentValidationError::InvalidTitle(
333                "Task title cannot be empty".to_string(),
334            ));
335        }
336
337        // Tasks should have a parent (Initiative) unless they are in Backlog phase
338        if self.parent_id().is_none() {
339            // Allow no parent only if task is in Backlog phase
340            if let Ok(phase) = self.phase() {
341                if phase != Phase::Backlog {
342                    return Err(DocumentValidationError::MissingRequiredField(
343                        "Tasks should have a parent Initiative unless in Backlog phase".to_string(),
344                    ));
345                }
346            } else {
347                return Err(DocumentValidationError::MissingRequiredField(
348                    "Tasks should have a parent Initiative".to_string(),
349                ));
350            }
351        }
352
353        // If blocked, must have blocking documents listed
354        if let Ok(Phase::Blocked) = self.phase() {
355            if self.blocked_by().is_empty() {
356                return Err(DocumentValidationError::InvalidContent(
357                    "Blocked tasks must specify what they are blocked by".to_string(),
358                ));
359            }
360        }
361
362        Ok(())
363    }
364
365    fn exit_criteria_met(&self) -> bool {
366        // Check if all acceptance criteria checkboxes are checked
367        // This would typically parse the content for checkbox completion
368        // For now, return false as a placeholder
369        false
370    }
371
372    fn template(&self) -> DocumentTemplate {
373        DocumentTemplate {
374            frontmatter: self.frontmatter_template(),
375            content: self.content_template(),
376            acceptance_criteria: self.acceptance_criteria_template(),
377            file_extension: "md",
378        }
379    }
380
381    fn frontmatter_template(&self) -> &'static str {
382        include_str!("frontmatter.yaml")
383    }
384
385    fn content_template(&self) -> &'static str {
386        include_str!("content.md")
387    }
388
389    fn acceptance_criteria_template(&self) -> &'static str {
390        include_str!("acceptance_criteria.md")
391    }
392
393    fn transition_phase(
394        &mut self,
395        target_phase: Option<Phase>,
396    ) -> Result<Phase, DocumentValidationError> {
397        let current_phase = self.phase()?;
398
399        let new_phase = match target_phase {
400            Some(phase) => {
401                // Validate the transition is allowed
402                if !self.can_transition_to(phase) {
403                    return Err(DocumentValidationError::InvalidPhaseTransition {
404                        from: current_phase,
405                        to: phase,
406                    });
407                }
408                phase
409            }
410            None => {
411                // Auto-transition to next phase in sequence
412                match Self::next_phase_in_sequence(current_phase) {
413                    Some(next) => next,
414                    None => return Ok(current_phase), // Already at final phase or blocked
415                }
416            }
417        };
418
419        self.update_phase_tag(new_phase);
420        Ok(new_phase)
421    }
422
423    fn core_mut(&mut self) -> &mut super::traits::DocumentCore {
424        &mut self.core
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use crate::domain::documents::traits::DocumentValidationError;
432    use tempfile::tempdir;
433
434    #[tokio::test]
435    async fn test_task_from_content() {
436        let content = r##"---
437id: test-task
438level: task
439title: "Test Task"
440created_at: 2025-01-01T00:00:00Z
441updated_at: 2025-01-01T00:00:00Z
442archived: false
443parent: initiative-001
444blocked_by: []
445
446tags:
447  - "#task"
448  - "#phase/todo"
449
450exit_criteria_met: false
451---
452
453# Test Task
454
455## Description
456
457This is a test task for our system.
458
459## Implementation Notes
460
461Details on how to implement this.
462
463## Acceptance Criteria
464
465- [ ] Implementation is complete
466- [ ] Tests pass
467"##;
468
469        let task = Task::from_content(content).unwrap();
470
471        assert_eq!(task.title(), "Test Task");
472        assert_eq!(task.document_type(), DocumentType::Task);
473        assert!(!task.archived());
474        assert_eq!(task.tags().len(), 2);
475        assert_eq!(task.phase().unwrap(), Phase::Todo);
476        assert!(task.content().has_acceptance_criteria());
477
478        // Round-trip test: write to file and read back
479        let temp_dir = tempdir().unwrap();
480        let file_path = temp_dir.path().join("test-task.md");
481
482        task.to_file(&file_path).await.unwrap();
483        let loaded_task = Task::from_file(&file_path).await.unwrap();
484
485        assert_eq!(loaded_task.title(), task.title());
486        assert_eq!(loaded_task.phase().unwrap(), task.phase().unwrap());
487        assert_eq!(loaded_task.content().body, task.content().body);
488        assert_eq!(loaded_task.archived(), task.archived());
489        assert_eq!(loaded_task.tags().len(), task.tags().len());
490    }
491
492    #[test]
493    fn test_task_invalid_level() {
494        let content = r##"---
495id: test-doc
496level: strategy
497title: "Test Strategy"
498created_at: 2025-01-01T00:00:00Z
499updated_at: 2025-01-01T00:00:00Z
500archived: false
501tags:
502  - "#strategy"
503  - "#phase/shaping"
504exit_criteria_met: false
505---
506
507# Test Strategy
508"##;
509
510        let result = Task::from_content(content);
511        assert!(result.is_err());
512        match result.unwrap_err() {
513            DocumentValidationError::InvalidContent(msg) => {
514                assert!(msg.contains("Expected level 'task'"));
515            }
516            _ => panic!("Expected InvalidContent error"),
517        }
518    }
519
520    #[test]
521    fn test_task_validation() {
522        let task = Task::new(
523            "Test Task".to_string(),
524            Some(DocumentId::from("parent-initiative")), // parent_id
525            Some("Parent Initiative".to_string()), // parent_title
526            Some(DocumentId::from("parent-strategy")), // strategy_id
527            Some(DocumentId::from("parent-initiative")), // initiative_id
528            vec![], // blocked_by
529            vec![Tag::Label("task".to_string()), Tag::Phase(Phase::Todo)],
530            false,
531        )
532        .expect("Failed to create task");
533
534        assert!(task.validate().is_ok());
535
536        // Test validation failure - no parent
537        let task_no_parent = Task::new(
538            "Test Task".to_string(),
539            None, // No parent
540            None, // No parent title
541            None, // No strategy
542            None, // No initiative
543            vec![], // blocked_by
544            vec![Tag::Phase(Phase::Todo)],
545            false,
546        )
547        .expect("Failed to create task");
548
549        assert!(task_no_parent.validate().is_err());
550    }
551
552    #[test]
553    fn test_task_blocked_validation() {
554        // Task marked as blocked but no blocking documents
555        let blocked_task = Task::new(
556            "Blocked Task".to_string(),
557            Some(DocumentId::from("parent-initiative")), // parent_id
558            Some("Parent Initiative".to_string()), // parent_title
559            Some(DocumentId::from("parent-strategy")), // strategy_id
560            Some(DocumentId::from("parent-initiative")), // initiative_id
561            vec![], // No blocking documents
562            vec![Tag::Phase(Phase::Blocked)],
563            false,
564        )
565        .expect("Failed to create task");
566
567        assert!(blocked_task.validate().is_err());
568
569        // Task marked as blocked with blocking documents
570        let properly_blocked_task = Task::new(
571            "Blocked Task".to_string(),
572            Some(DocumentId::from("parent-initiative")), // parent_id
573            Some("Parent Initiative".to_string()), // parent_title
574            Some(DocumentId::from("parent-strategy")), // strategy_id
575            Some(DocumentId::from("parent-initiative")), // initiative_id
576            vec![DocumentId::from("blocking-task")],
577            vec![Tag::Phase(Phase::Blocked)],
578            false,
579        )
580        .expect("Failed to create task");
581
582        assert!(properly_blocked_task.validate().is_ok());
583    }
584
585    #[test]
586    fn test_task_phase_transitions() {
587        let task = Task::new(
588            "Test Task".to_string(),
589            Some(DocumentId::from("parent-initiative")), // parent_id
590            Some("Parent Initiative".to_string()), // parent_title
591            Some(DocumentId::from("parent-strategy")), // strategy_id
592            Some(DocumentId::from("parent-initiative")), // initiative_id
593            vec![],
594            vec![Tag::Phase(Phase::Todo)],
595            false,
596        )
597        .expect("Failed to create task");
598
599        assert!(task.can_transition_to(Phase::Active));
600        assert!(task.can_transition_to(Phase::Blocked));
601        assert!(!task.can_transition_to(Phase::Completed));
602        assert!(!task.can_transition_to(Phase::Design));
603    }
604
605    #[test]
606    fn test_task_active_phase_transitions() {
607        let active_task = Task::new(
608            "Active Task".to_string(),
609            Some(DocumentId::from("parent-initiative")), // parent_id
610            Some("Parent Initiative".to_string()), // parent_title
611            Some(DocumentId::from("parent-strategy")), // strategy_id
612            Some(DocumentId::from("parent-initiative")), // initiative_id
613            vec![],
614            vec![Tag::Phase(Phase::Active)],
615            false,
616        )
617        .expect("Failed to create task");
618
619        assert!(active_task.can_transition_to(Phase::Completed));
620        assert!(active_task.can_transition_to(Phase::Blocked));
621        assert!(!active_task.can_transition_to(Phase::Todo));
622    }
623
624    #[test]
625    fn test_task_blocked_phase_transitions() {
626        let blocked_task = Task::new(
627            "Blocked Task".to_string(),
628            Some(DocumentId::from("parent-initiative")), // parent_id
629            Some("Parent Initiative".to_string()), // parent_title
630            Some(DocumentId::from("parent-strategy")), // strategy_id
631            Some(DocumentId::from("parent-initiative")), // initiative_id
632            vec![DocumentId::from("blocking-task")],
633            vec![Tag::Phase(Phase::Blocked)],
634            false,
635        )
636        .expect("Failed to create task");
637
638        assert!(blocked_task.can_transition_to(Phase::Active));
639        assert!(blocked_task.can_transition_to(Phase::Todo));
640        assert!(!blocked_task.can_transition_to(Phase::Completed));
641    }
642
643    #[test]
644    fn test_task_transition_phase_auto() {
645        let mut task = Task::new(
646            "Test Task".to_string(),
647            Some(DocumentId::from("parent-initiative")), // parent_id
648            Some("Parent Initiative".to_string()), // parent_title
649            Some(DocumentId::from("parent-strategy")), // strategy_id
650            Some(DocumentId::from("parent-initiative")), // initiative_id
651            vec![],
652            vec![Tag::Phase(Phase::Todo)],
653            false,
654        )
655        .expect("Failed to create task");
656
657        // Auto-transition from Todo should go to Active
658        let new_phase = task.transition_phase(None).unwrap();
659        assert_eq!(new_phase, Phase::Active);
660        assert_eq!(task.phase().unwrap(), Phase::Active);
661
662        // Auto-transition from Active should go to Completed
663        let new_phase = task.transition_phase(None).unwrap();
664        assert_eq!(new_phase, Phase::Completed);
665        assert_eq!(task.phase().unwrap(), Phase::Completed);
666
667        // Auto-transition from Completed should stay at Completed (final phase)
668        let new_phase = task.transition_phase(None).unwrap();
669        assert_eq!(new_phase, Phase::Completed);
670        assert_eq!(task.phase().unwrap(), Phase::Completed);
671    }
672
673    #[test]
674    fn test_task_transition_phase_blocking() {
675        let mut task = Task::new(
676            "Test Task".to_string(),
677            Some(DocumentId::from("parent-initiative")), // parent_id
678            Some("Parent Initiative".to_string()), // parent_title
679            Some(DocumentId::from("parent-strategy")), // strategy_id
680            Some(DocumentId::from("parent-initiative")), // initiative_id
681            vec![DocumentId::from("blocking-task")],
682            vec![Tag::Phase(Phase::Todo)],
683            false,
684        )
685        .expect("Failed to create task");
686
687        // Explicit transition from Todo to Blocked
688        let new_phase = task.transition_phase(Some(Phase::Blocked)).unwrap();
689        assert_eq!(new_phase, Phase::Blocked);
690        assert_eq!(task.phase().unwrap(), Phase::Blocked);
691
692        // Transition from Blocked back to Active (unblocking)
693        let new_phase = task.transition_phase(Some(Phase::Active)).unwrap();
694        assert_eq!(new_phase, Phase::Active);
695        assert_eq!(task.phase().unwrap(), Phase::Active);
696
697        // Blocked doesn't auto-transition
698        task.core.tags.retain(|tag| !matches!(tag, Tag::Phase(_)));
699        task.core.tags.push(Tag::Phase(Phase::Blocked));
700        let new_phase = task.transition_phase(None).unwrap();
701        assert_eq!(new_phase, Phase::Blocked); // Should stay blocked
702    }
703
704    #[test]
705    fn test_task_transition_phase_invalid() {
706        let mut task = Task::new(
707            "Test Task".to_string(),
708            Some(DocumentId::from("parent-initiative")), // parent_id
709            Some("Parent Initiative".to_string()), // parent_title
710            Some(DocumentId::from("parent-strategy")), // strategy_id
711            Some(DocumentId::from("parent-initiative")), // initiative_id
712            vec![],
713            vec![Tag::Phase(Phase::Todo)],
714            false,
715        )
716        .expect("Failed to create task");
717
718        // Invalid transition from Todo to Completed (must go through Active)
719        let result = task.transition_phase(Some(Phase::Completed));
720        assert!(result.is_err());
721        match result.unwrap_err() {
722            DocumentValidationError::InvalidPhaseTransition { from, to } => {
723                assert_eq!(from, Phase::Todo);
724                assert_eq!(to, Phase::Completed);
725            }
726            _ => panic!("Expected InvalidPhaseTransition error"),
727        }
728
729        // Should still be in Todo phase
730        assert_eq!(task.phase().unwrap(), Phase::Todo);
731    }
732
733    #[test]
734    fn test_task_update_section() {
735        // First create a task with the template
736        let mut task = Task::new(
737            "Test Task".to_string(),
738            Some(DocumentId::from("parent-initiative")), // parent_id
739            Some("Parent Initiative".to_string()), // parent_title
740            Some(DocumentId::from("parent-strategy")), // strategy_id
741            Some(DocumentId::from("parent-initiative")), // initiative_id
742            vec![],
743            vec![Tag::Phase(Phase::Todo)],
744            false,
745        )
746        .expect("Failed to create task");
747
748        // Then update its content to have specific test content
749        task.core_mut().content = DocumentContent::new(
750            "## Description\n\nOriginal description\n\n## Implementation Notes\n\nOriginal notes",
751        );
752
753        // Replace existing section
754        task.update_section("Updated task description", "Description", false)
755            .unwrap();
756        let content = task.content().body.clone();
757        assert!(content.contains("## Description\n\nUpdated task description"));
758        assert!(!content.contains("Original description"));
759
760        // Append to existing section
761        task.update_section(
762            "Additional implementation details",
763            "Implementation Notes",
764            true,
765        )
766        .unwrap();
767        let content = task.content().body.clone();
768        assert!(content.contains("Original notes"));
769        assert!(content.contains("Additional implementation details"));
770
771        // Add new section
772        task.update_section("Test approach details", "Testing Strategy", false)
773            .unwrap();
774        let content = task.content().body.clone();
775        assert!(content.contains("## Testing Strategy\n\nTest approach details"));
776    }
777}