1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use crate::types::RiskLevel;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PlanStatus {
17 Generating,
19 PendingReview,
21 Editing,
23 Executing,
25 Completed,
27 Failed,
29 Cancelled,
31}
32
33impl std::fmt::Display for PlanStatus {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 PlanStatus::Generating => write!(f, "generating"),
37 PlanStatus::PendingReview => write!(f, "pending review"),
38 PlanStatus::Editing => write!(f, "editing"),
39 PlanStatus::Executing => write!(f, "executing"),
40 PlanStatus::Completed => write!(f, "completed"),
41 PlanStatus::Failed => write!(f, "failed"),
42 PlanStatus::Cancelled => write!(f, "cancelled"),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum StepStatus {
51 #[default]
52 Pending,
53 InProgress,
54 Completed,
55 Failed,
56 Skipped,
57}
58
59impl std::fmt::Display for StepStatus {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 StepStatus::Pending => write!(f, "pending"),
63 StepStatus::InProgress => write!(f, "in progress"),
64 StepStatus::Completed => write!(f, "completed"),
65 StepStatus::Failed => write!(f, "failed"),
66 StepStatus::Skipped => write!(f, "skipped"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct PlanStep {
74 pub index: usize,
76 pub description: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub tool: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub tool_args: Option<serde_json::Value>,
84 #[serde(default)]
86 pub depends_on: Vec<usize>,
87 #[serde(default)]
89 pub status: StepStatus,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub result: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub risk_level: Option<RiskLevel>,
96 #[serde(default)]
98 pub requires_approval: bool,
99}
100
101impl Default for PlanStep {
102 fn default() -> Self {
103 Self {
104 index: 0,
105 description: String::new(),
106 tool: None,
107 tool_args: None,
108 depends_on: Vec::new(),
109 status: StepStatus::Pending,
110 result: None,
111 risk_level: None,
112 requires_approval: false,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct PlanAlternative {
120 pub name: String,
121 pub description: String,
122 #[serde(default)]
123 pub reason_not_chosen: String,
124 #[serde(default)]
125 pub estimated_steps: usize,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ExecutionPlan {
131 pub id: Uuid,
133 pub goal: String,
135 pub summary: String,
137 pub steps: Vec<PlanStep>,
139 #[serde(default)]
141 pub alternatives: Vec<PlanAlternative>,
142 #[serde(default)]
144 pub clarifications: Vec<String>,
145 pub status: PlanStatus,
147 pub created_at: DateTime<Utc>,
149 pub updated_at: DateTime<Utc>,
151 #[serde(default)]
153 pub current_step: Option<usize>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub estimated_cost: Option<f64>,
157 #[serde(default)]
159 pub council_generated: bool,
160}
161
162impl ExecutionPlan {
163 pub fn new(goal: impl Into<String>, summary: impl Into<String>) -> Self {
165 let now = Utc::now();
166 Self {
167 id: Uuid::new_v4(),
168 goal: goal.into(),
169 summary: summary.into(),
170 steps: Vec::new(),
171 alternatives: Vec::new(),
172 clarifications: Vec::new(),
173 status: PlanStatus::Generating,
174 created_at: now,
175 updated_at: now,
176 current_step: None,
177 estimated_cost: None,
178 council_generated: false,
179 }
180 }
181
182 pub fn next_pending_step(&self) -> Option<usize> {
184 for step in &self.steps {
185 if step.status == StepStatus::Pending && self.dependencies_met(step.index) {
186 return Some(step.index);
187 }
188 }
189 None
190 }
191
192 pub fn dependencies_met(&self, step_index: usize) -> bool {
194 if let Some(step) = self.steps.get(step_index) {
195 step.depends_on.iter().all(|&dep| {
196 self.steps
197 .get(dep)
198 .map(|s| s.status == StepStatus::Completed)
199 .unwrap_or(false)
200 })
201 } else {
202 false
203 }
204 }
205
206 pub fn complete_step(&mut self, step_index: usize, result: impl Into<String>) {
208 if let Some(step) = self.steps.get_mut(step_index) {
209 step.status = StepStatus::Completed;
210 step.result = Some(result.into());
211 self.updated_at = Utc::now();
212 }
213 }
214
215 pub fn fail_step(&mut self, step_index: usize, error: impl Into<String>) {
217 if let Some(step) = self.steps.get_mut(step_index) {
218 step.status = StepStatus::Failed;
219 step.result = Some(error.into());
220 self.updated_at = Utc::now();
221 }
222 }
223
224 pub fn progress_summary(&self) -> String {
226 let completed = self
227 .steps
228 .iter()
229 .filter(|s| s.status == StepStatus::Completed)
230 .count();
231 let failed = self
232 .steps
233 .iter()
234 .filter(|s| s.status == StepStatus::Failed)
235 .count();
236 let total = self.steps.len();
237
238 if failed > 0 {
239 format!(
240 "{}/{} steps completed ({} failed) — {}",
241 completed, total, failed, self.status
242 )
243 } else {
244 format!("{}/{} steps completed — {}", completed, total, self.status)
245 }
246 }
247}
248
249impl std::fmt::Display for ExecutionPlan {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 writeln!(f, "Plan: {}", self.goal)?;
252 writeln!(f, "Summary: {}", self.summary)?;
253 writeln!(f, "Status: {}", self.status)?;
254 writeln!(f)?;
255
256 for step in &self.steps {
257 let icon = match step.status {
258 StepStatus::Pending => "○",
259 StepStatus::InProgress => "●",
260 StepStatus::Completed => "✓",
261 StepStatus::Failed => "✗",
262 StepStatus::Skipped => "⊘",
263 };
264
265 let tool_info = step
266 .tool
267 .as_deref()
268 .map(|t| format!(" [{}]", t))
269 .unwrap_or_default();
270
271 let risk_badge = step
272 .risk_level
273 .as_ref()
274 .map(|r| format!(" ({})", r))
275 .unwrap_or_default();
276
277 let approval = if step.requires_approval {
278 " ⚠ approval"
279 } else {
280 ""
281 };
282
283 writeln!(
284 f,
285 " {} {}. {}{}{}{}",
286 icon,
287 step.index + 1,
288 step.description,
289 tool_info,
290 risk_badge,
291 approval
292 )?;
293 }
294
295 if !self.alternatives.is_empty() {
296 writeln!(f)?;
297 writeln!(f, "Alternatives considered:")?;
298 for alt in &self.alternatives {
299 writeln!(f, " - {} ({})", alt.name, alt.reason_not_chosen)?;
300 }
301 }
302
303 if let Some(cost) = self.estimated_cost {
304 writeln!(f)?;
305 writeln!(f, "Estimated cost: ${:.4}", cost)?;
306 }
307
308 Ok(())
309 }
310}
311
312#[derive(Debug, Clone)]
314pub enum PlanDecision {
315 Approve,
317 Reject,
319 EditStep(usize, String),
321 RemoveStep(usize),
323 AddStep(usize, String),
325 ReorderSteps(Vec<usize>),
327 AskQuestion(String),
329}
330
331pub const PLAN_GENERATION_PROMPT: &str = r#"You are generating a structured execution plan. Respond ONLY with valid JSON (no markdown fences, no extra text).
333
334The JSON must have this structure:
335{
336 "summary": "Brief 1-2 sentence summary of the approach",
337 "steps": [
338 {
339 "description": "Human-readable description of what this step does",
340 "tool": "tool_name or null if no specific tool",
341 "tool_args": { ... } or null if tool arguments should be determined at execution time,
342 "depends_on": [0, 1] (indices of prerequisite steps, empty array if none),
343 "risk_level": "read_only" | "write" | "execute" | "network" | "destructive" | null,
344 "requires_approval": false
345 }
346 ],
347 "alternatives": [
348 {
349 "name": "Alternative approach name",
350 "description": "What it would do differently",
351 "reason_not_chosen": "Why the chosen approach is better",
352 "estimated_steps": 5
353 }
354 ],
355 "clarifications": ["Question for the user if anything is unclear"],
356 "estimated_cost": 0.05
357}
358
359Guidelines:
360- Keep plans concise: prefer fewer, well-described steps over many granular ones
361- Maximum 20 steps
362- Set requires_approval=true for destructive or irreversible operations
363- Include tool_args only when you are confident about the values
364- Use depends_on to express execution order dependencies
365- List alternatives only when meaningfully different approaches exist
366- Include clarifications only for genuinely ambiguous requirements
367"#;
368
369pub fn parse_plan_json(text: &str, goal: &str) -> ExecutionPlan {
376 let cleaned = strip_code_fences(text);
378
379 match serde_json::from_str::<serde_json::Value>(&cleaned) {
381 Ok(value) => build_plan_from_json(value, goal),
382 Err(_) => {
383 let fixed = strip_trailing_commas(&cleaned);
385 match serde_json::from_str::<serde_json::Value>(&fixed) {
386 Ok(value) => build_plan_from_json(value, goal),
387 Err(_) => fallback_single_step_plan(goal, text),
388 }
389 }
390 }
391}
392
393fn strip_code_fences(text: &str) -> String {
395 let trimmed = text.trim();
396 if let Some(rest) = trimmed.strip_prefix("```json")
397 && let Some(inner) = rest.strip_suffix("```")
398 {
399 return inner.trim().to_string();
400 }
401 if let Some(rest) = trimmed.strip_prefix("```")
402 && let Some(inner) = rest.strip_suffix("```")
403 {
404 return inner.trim().to_string();
405 }
406 trimmed.to_string()
407}
408
409fn strip_trailing_commas(text: &str) -> String {
411 let mut result = String::with_capacity(text.len());
412 let chars: Vec<char> = text.chars().collect();
413 let len = chars.len();
414
415 let mut i = 0;
416 while i < len {
417 if chars[i] == ',' {
418 let mut j = i + 1;
420 while j < len && chars[j].is_whitespace() {
421 j += 1;
422 }
423 if j < len && (chars[j] == '}' || chars[j] == ']') {
424 i += 1;
426 continue;
427 }
428 }
429 result.push(chars[i]);
430 i += 1;
431 }
432 result
433}
434
435fn build_plan_from_json(value: serde_json::Value, goal: &str) -> ExecutionPlan {
437 let summary = value["summary"]
438 .as_str()
439 .unwrap_or("Plan generated by LLM")
440 .to_string();
441
442 let mut steps = Vec::new();
443 if let Some(step_arr) = value["steps"].as_array() {
444 for (i, step_val) in step_arr.iter().enumerate() {
445 let desc = step_val["description"]
446 .as_str()
447 .unwrap_or("(no description)")
448 .to_string();
449
450 let tool = step_val["tool"].as_str().map(|s| s.to_string());
451
452 let tool_args = if step_val["tool_args"].is_null() {
453 None
454 } else {
455 Some(step_val["tool_args"].clone())
456 };
457
458 let depends_on = step_val["depends_on"]
459 .as_array()
460 .map(|arr| {
461 arr.iter()
462 .filter_map(|v| v.as_u64().map(|n| n as usize))
463 .collect()
464 })
465 .unwrap_or_default();
466
467 let risk_level = step_val["risk_level"].as_str().and_then(parse_risk_level);
468
469 let requires_approval = step_val["requires_approval"].as_bool().unwrap_or(false);
470
471 steps.push(PlanStep {
472 index: i,
473 description: desc,
474 tool,
475 tool_args,
476 depends_on,
477 status: StepStatus::Pending,
478 result: None,
479 risk_level,
480 requires_approval,
481 });
482 }
483 }
484
485 let mut alternatives = Vec::new();
486 if let Some(alt_arr) = value["alternatives"].as_array() {
487 for alt_val in alt_arr {
488 alternatives.push(PlanAlternative {
489 name: alt_val["name"]
490 .as_str()
491 .unwrap_or("Alternative")
492 .to_string(),
493 description: alt_val["description"].as_str().unwrap_or("").to_string(),
494 reason_not_chosen: alt_val["reason_not_chosen"]
495 .as_str()
496 .unwrap_or("")
497 .to_string(),
498 estimated_steps: alt_val["estimated_steps"].as_u64().unwrap_or(0) as usize,
499 });
500 }
501 }
502
503 let clarifications = value["clarifications"]
504 .as_array()
505 .map(|arr| {
506 arr.iter()
507 .filter_map(|v| v.as_str().map(|s| s.to_string()))
508 .collect()
509 })
510 .unwrap_or_default();
511
512 let estimated_cost = value["estimated_cost"].as_f64();
513
514 let now = Utc::now();
515 ExecutionPlan {
516 id: Uuid::new_v4(),
517 goal: goal.to_string(),
518 summary,
519 steps,
520 alternatives,
521 clarifications,
522 status: PlanStatus::PendingReview,
523 created_at: now,
524 updated_at: now,
525 current_step: None,
526 estimated_cost,
527 council_generated: false,
528 }
529}
530
531fn parse_risk_level(s: &str) -> Option<RiskLevel> {
533 match s {
534 "read_only" => Some(RiskLevel::ReadOnly),
535 "write" => Some(RiskLevel::Write),
536 "execute" => Some(RiskLevel::Execute),
537 "network" => Some(RiskLevel::Network),
538 "destructive" => Some(RiskLevel::Destructive),
539 _ => None,
540 }
541}
542
543fn fallback_single_step_plan(goal: &str, raw_text: &str) -> ExecutionPlan {
545 let now = Utc::now();
546 let description = if raw_text.len() > 200 {
547 format!("Execute task as described: {}...", &raw_text[..200])
548 } else {
549 format!("Execute task as described: {}", raw_text)
550 };
551
552 ExecutionPlan {
553 id: Uuid::new_v4(),
554 goal: goal.to_string(),
555 summary: "Single-step execution (plan parsing failed, using fallback)".to_string(),
556 steps: vec![PlanStep {
557 index: 0,
558 description,
559 tool: None,
560 tool_args: None,
561 depends_on: Vec::new(),
562 status: StepStatus::Pending,
563 result: None,
564 risk_level: None,
565 requires_approval: false,
566 }],
567 alternatives: Vec::new(),
568 clarifications: Vec::new(),
569 status: PlanStatus::PendingReview,
570 created_at: now,
571 updated_at: now,
572 current_step: None,
573 estimated_cost: None,
574 council_generated: false,
575 }
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct PlanConfig {
581 pub enabled: bool,
583 pub use_council: bool,
585 pub max_steps: usize,
587 pub auto_approve_readonly: bool,
589}
590
591impl Default for PlanConfig {
592 fn default() -> Self {
593 Self {
594 enabled: false,
595 use_council: false,
596 max_steps: 20,
597 auto_approve_readonly: false,
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_plan_status_display() {
608 assert_eq!(PlanStatus::Generating.to_string(), "generating");
609 assert_eq!(PlanStatus::PendingReview.to_string(), "pending review");
610 assert_eq!(PlanStatus::Executing.to_string(), "executing");
611 assert_eq!(PlanStatus::Completed.to_string(), "completed");
612 assert_eq!(PlanStatus::Failed.to_string(), "failed");
613 assert_eq!(PlanStatus::Cancelled.to_string(), "cancelled");
614 }
615
616 #[test]
617 fn test_step_status_default() {
618 assert_eq!(StepStatus::default(), StepStatus::Pending);
619 }
620
621 #[test]
622 fn test_plan_step_default() {
623 let step = PlanStep::default();
624 assert_eq!(step.index, 0);
625 assert!(step.description.is_empty());
626 assert!(step.tool.is_none());
627 assert!(step.tool_args.is_none());
628 assert!(step.depends_on.is_empty());
629 assert_eq!(step.status, StepStatus::Pending);
630 assert!(step.result.is_none());
631 assert!(!step.requires_approval);
632 }
633
634 #[test]
635 fn test_execution_plan_new() {
636 let plan = ExecutionPlan::new("Refactor auth module", "Split into separate files");
637 assert_eq!(plan.goal, "Refactor auth module");
638 assert_eq!(plan.summary, "Split into separate files");
639 assert_eq!(plan.status, PlanStatus::Generating);
640 assert!(plan.steps.is_empty());
641 assert!(plan.current_step.is_none());
642 }
643
644 #[test]
645 fn test_next_pending_step() {
646 let mut plan = ExecutionPlan::new("test", "test");
647 plan.steps = vec![
648 PlanStep {
649 index: 0,
650 description: "Step 0".into(),
651 status: StepStatus::Completed,
652 ..Default::default()
653 },
654 PlanStep {
655 index: 1,
656 description: "Step 1".into(),
657 depends_on: vec![0],
658 ..Default::default()
659 },
660 PlanStep {
661 index: 2,
662 description: "Step 2".into(),
663 depends_on: vec![1],
664 ..Default::default()
665 },
666 ];
667
668 assert_eq!(plan.next_pending_step(), Some(1));
670 }
671
672 #[test]
673 fn test_next_pending_step_no_deps() {
674 let mut plan = ExecutionPlan::new("test", "test");
675 plan.steps = vec![
676 PlanStep {
677 index: 0,
678 description: "Step 0".into(),
679 ..Default::default()
680 },
681 PlanStep {
682 index: 1,
683 description: "Step 1".into(),
684 ..Default::default()
685 },
686 ];
687
688 assert_eq!(plan.next_pending_step(), Some(0));
689 }
690
691 #[test]
692 fn test_next_pending_step_all_completed() {
693 let mut plan = ExecutionPlan::new("test", "test");
694 plan.steps = vec![PlanStep {
695 index: 0,
696 description: "Step 0".into(),
697 status: StepStatus::Completed,
698 ..Default::default()
699 }];
700
701 assert_eq!(plan.next_pending_step(), None);
702 }
703
704 #[test]
705 fn test_dependencies_met() {
706 let mut plan = ExecutionPlan::new("test", "test");
707 plan.steps = vec![
708 PlanStep {
709 index: 0,
710 status: StepStatus::Completed,
711 ..Default::default()
712 },
713 PlanStep {
714 index: 1,
715 depends_on: vec![0],
716 ..Default::default()
717 },
718 PlanStep {
719 index: 2,
720 depends_on: vec![0, 1],
721 ..Default::default()
722 },
723 ];
724
725 assert!(plan.dependencies_met(0)); assert!(plan.dependencies_met(1)); assert!(!plan.dependencies_met(2)); }
729
730 #[test]
731 fn test_complete_step() {
732 let mut plan = ExecutionPlan::new("test", "test");
733 plan.steps = vec![PlanStep {
734 index: 0,
735 description: "Read file".into(),
736 ..Default::default()
737 }];
738
739 plan.complete_step(0, "File contents: hello world");
740 assert_eq!(plan.steps[0].status, StepStatus::Completed);
741 assert_eq!(
742 plan.steps[0].result.as_deref(),
743 Some("File contents: hello world")
744 );
745 }
746
747 #[test]
748 fn test_fail_step() {
749 let mut plan = ExecutionPlan::new("test", "test");
750 plan.steps = vec![PlanStep {
751 index: 0,
752 description: "Write file".into(),
753 ..Default::default()
754 }];
755
756 plan.fail_step(0, "Permission denied");
757 assert_eq!(plan.steps[0].status, StepStatus::Failed);
758 assert_eq!(plan.steps[0].result.as_deref(), Some("Permission denied"));
759 }
760
761 #[test]
762 fn test_progress_summary() {
763 let mut plan = ExecutionPlan::new("test", "test");
764 plan.status = PlanStatus::Executing;
765 plan.steps = vec![
766 PlanStep {
767 index: 0,
768 status: StepStatus::Completed,
769 ..Default::default()
770 },
771 PlanStep {
772 index: 1,
773 status: StepStatus::Completed,
774 ..Default::default()
775 },
776 PlanStep {
777 index: 2,
778 status: StepStatus::Failed,
779 ..Default::default()
780 },
781 PlanStep {
782 index: 3,
783 ..Default::default()
784 },
785 ];
786
787 let summary = plan.progress_summary();
788 assert!(summary.contains("2/4"));
789 assert!(summary.contains("1 failed"));
790 }
791
792 #[test]
793 fn test_progress_summary_no_failures() {
794 let mut plan = ExecutionPlan::new("test", "test");
795 plan.status = PlanStatus::Executing;
796 plan.steps = vec![
797 PlanStep {
798 index: 0,
799 status: StepStatus::Completed,
800 ..Default::default()
801 },
802 PlanStep {
803 index: 1,
804 ..Default::default()
805 },
806 ];
807
808 let summary = plan.progress_summary();
809 assert!(summary.contains("1/2"));
810 assert!(!summary.contains("failed"));
811 }
812
813 #[test]
814 fn test_parse_plan_json_valid() {
815 let json = r#"{
816 "summary": "Read and analyze a file",
817 "steps": [
818 {
819 "description": "Read the source file",
820 "tool": "file_read",
821 "tool_args": {"path": "src/main.rs"},
822 "depends_on": [],
823 "risk_level": "read_only",
824 "requires_approval": false
825 },
826 {
827 "description": "Analyze the code structure",
828 "tool": null,
829 "tool_args": null,
830 "depends_on": [0],
831 "risk_level": null,
832 "requires_approval": false
833 }
834 ],
835 "alternatives": [
836 {
837 "name": "Use codebase_search",
838 "description": "Search for patterns instead of reading the whole file",
839 "reason_not_chosen": "Direct file read is more thorough",
840 "estimated_steps": 3
841 }
842 ],
843 "clarifications": [],
844 "estimated_cost": 0.02
845 }"#;
846
847 let plan = parse_plan_json(json, "Analyze main.rs");
848 assert_eq!(plan.goal, "Analyze main.rs");
849 assert_eq!(plan.summary, "Read and analyze a file");
850 assert_eq!(plan.steps.len(), 2);
851 assert_eq!(plan.steps[0].tool.as_deref(), Some("file_read"));
852 assert_eq!(plan.steps[0].risk_level, Some(RiskLevel::ReadOnly));
853 assert_eq!(plan.steps[1].depends_on, vec![0]);
854 assert!(plan.steps[1].tool.is_none());
855 assert_eq!(plan.alternatives.len(), 1);
856 assert_eq!(plan.estimated_cost, Some(0.02));
857 assert_eq!(plan.status, PlanStatus::PendingReview);
858 }
859
860 #[test]
861 fn test_parse_plan_json_with_code_fences() {
862 let json = r#"```json
863{
864 "summary": "Simple plan",
865 "steps": [
866 {
867 "description": "Do something",
868 "tool": null,
869 "tool_args": null,
870 "depends_on": [],
871 "risk_level": null,
872 "requires_approval": false
873 }
874 ],
875 "alternatives": [],
876 "clarifications": []
877}
878```"#;
879
880 let plan = parse_plan_json(json, "Test task");
881 assert_eq!(plan.summary, "Simple plan");
882 assert_eq!(plan.steps.len(), 1);
883 }
884
885 #[test]
886 fn test_parse_plan_json_with_trailing_commas() {
887 let json = r#"{
888 "summary": "Plan with trailing commas",
889 "steps": [
890 {
891 "description": "Step one",
892 "tool": null,
893 "tool_args": null,
894 "depends_on": [],
895 "risk_level": null,
896 "requires_approval": false,
897 },
898 ],
899 "alternatives": [],
900 "clarifications": [],
901 }"#;
902
903 let plan = parse_plan_json(json, "Test");
904 assert_eq!(plan.steps.len(), 1);
905 assert_eq!(plan.steps[0].description, "Step one");
906 }
907
908 #[test]
909 fn test_parse_plan_json_invalid_fallback() {
910 let text = "This is not JSON at all, it's just a description of what to do.";
911 let plan = parse_plan_json(text, "Do something");
912 assert_eq!(plan.goal, "Do something");
913 assert_eq!(plan.steps.len(), 1);
914 assert!(plan.summary.contains("fallback"));
915 assert!(
916 plan.steps[0]
917 .description
918 .contains("Execute task as described")
919 );
920 }
921
922 #[test]
923 fn test_serialization_roundtrip() {
924 let mut plan = ExecutionPlan::new("Test goal", "Test summary");
925 plan.steps = vec![
926 PlanStep {
927 index: 0,
928 description: "Step 0".into(),
929 tool: Some("file_read".into()),
930 risk_level: Some(RiskLevel::ReadOnly),
931 ..Default::default()
932 },
933 PlanStep {
934 index: 1,
935 description: "Step 1".into(),
936 depends_on: vec![0],
937 requires_approval: true,
938 ..Default::default()
939 },
940 ];
941 plan.status = PlanStatus::PendingReview;
942
943 let json = serde_json::to_string(&plan).unwrap();
944 let deserialized: ExecutionPlan = serde_json::from_str(&json).unwrap();
945
946 assert_eq!(deserialized.goal, "Test goal");
947 assert_eq!(deserialized.steps.len(), 2);
948 assert_eq!(deserialized.steps[0].tool.as_deref(), Some("file_read"));
949 assert_eq!(deserialized.steps[1].depends_on, vec![0]);
950 assert!(deserialized.steps[1].requires_approval);
951 }
952
953 #[test]
954 fn test_plan_display() {
955 let mut plan = ExecutionPlan::new("Refactor auth", "Split into modules");
956 plan.status = PlanStatus::Executing;
957 plan.steps = vec![
958 PlanStep {
959 index: 0,
960 description: "Read auth.rs".into(),
961 tool: Some("file_read".into()),
962 status: StepStatus::Completed,
963 risk_level: Some(RiskLevel::ReadOnly),
964 ..Default::default()
965 },
966 PlanStep {
967 index: 1,
968 description: "Create auth/mod.rs".into(),
969 tool: Some("file_write".into()),
970 status: StepStatus::InProgress,
971 risk_level: Some(RiskLevel::Write),
972 requires_approval: true,
973 ..Default::default()
974 },
975 PlanStep {
976 index: 2,
977 description: "Update imports".into(),
978 status: StepStatus::Pending,
979 ..Default::default()
980 },
981 ];
982
983 let display = format!("{}", plan);
984 assert!(display.contains("Refactor auth"));
985 assert!(display.contains("Split into modules"));
986 assert!(display.contains("✓")); assert!(display.contains("●")); assert!(display.contains("○")); assert!(display.contains("[file_read]"));
990 assert!(display.contains("approval"));
991 }
992
993 #[test]
994 fn test_plan_config_defaults() {
995 let config = PlanConfig::default();
996 assert!(!config.enabled);
997 assert!(!config.use_council);
998 assert_eq!(config.max_steps, 20);
999 assert!(!config.auto_approve_readonly);
1000 }
1001
1002 #[test]
1003 fn test_plan_config_serialization() {
1004 let config = PlanConfig {
1005 enabled: true,
1006 use_council: true,
1007 max_steps: 10,
1008 auto_approve_readonly: true,
1009 };
1010 let json = serde_json::to_string(&config).unwrap();
1011 let deserialized: PlanConfig = serde_json::from_str(&json).unwrap();
1012 assert!(deserialized.enabled);
1013 assert!(deserialized.use_council);
1014 assert_eq!(deserialized.max_steps, 10);
1015 assert!(deserialized.auto_approve_readonly);
1016 }
1017
1018 #[test]
1019 fn test_strip_code_fences() {
1020 assert_eq!(strip_code_fences("```json\n{\"a\":1}\n```"), "{\"a\":1}");
1021 assert_eq!(strip_code_fences("```\n{\"a\":1}\n```"), "{\"a\":1}");
1022 assert_eq!(strip_code_fences("{\"a\":1}"), "{\"a\":1}");
1023 }
1024
1025 #[test]
1026 fn test_strip_trailing_commas() {
1027 assert_eq!(strip_trailing_commas("{\"a\":1,}"), "{\"a\":1}");
1028 assert_eq!(strip_trailing_commas("[1,2,]"), "[1,2]");
1029 assert_eq!(strip_trailing_commas("{\"a\":[1,],}"), "{\"a\":[1]}");
1030 assert_eq!(
1032 strip_trailing_commas("{\"a\":1,\"b\":2}"),
1033 "{\"a\":1,\"b\":2}"
1034 );
1035 }
1036
1037 #[test]
1038 fn test_parse_plan_json_minimal() {
1039 let json = r#"{"summary": "Do it", "steps": []}"#;
1041 let plan = parse_plan_json(json, "Goal");
1042 assert_eq!(plan.summary, "Do it");
1043 assert!(plan.steps.is_empty());
1044 }
1045
1046 #[test]
1047 fn test_complete_step_out_of_bounds() {
1048 let mut plan = ExecutionPlan::new("test", "test");
1049 plan.steps = vec![PlanStep {
1050 index: 0,
1051 ..Default::default()
1052 }];
1053 plan.complete_step(99, "result");
1055 assert_eq!(plan.steps[0].status, StepStatus::Pending);
1056 }
1057
1058 #[test]
1059 fn test_dependencies_met_out_of_bounds() {
1060 let plan = ExecutionPlan::new("test", "test");
1061 assert!(!plan.dependencies_met(99));
1062 }
1063}