oven_cli/agents/
planner.rs1use anyhow::{Context, Result};
2use askama::Template;
3
4use crate::{agents::GraphContextNode, issues::PipelineIssue};
5
6#[derive(Template)]
7#[template(path = "planner.txt")]
8struct PlannerPrompt<'a> {
9 issues: &'a [PipelineIssue],
10 graph_context: &'a [GraphContextNode],
11}
12
13pub fn build_prompt(
14 issues: &[PipelineIssue],
15 graph_context: &[GraphContextNode],
16) -> Result<String> {
17 let tmpl = PlannerPrompt { issues, graph_context };
18 tmpl.render().context("rendering planner template")
19}
20
21#[cfg(test)]
22mod tests {
23 use super::*;
24 use crate::issues::IssueOrigin;
25
26 fn sample_issues() -> Vec<PipelineIssue> {
27 vec![
28 PipelineIssue {
29 number: 1,
30 title: "Add login".to_string(),
31 body: "implement login flow".to_string(),
32 source: IssueOrigin::Github,
33 target_repo: None,
34 author: None,
35 },
36 PipelineIssue {
37 number: 2,
38 title: "Fix bug".to_string(),
39 body: "crash on startup".to_string(),
40 source: IssueOrigin::Github,
41 target_repo: None,
42 author: None,
43 },
44 ]
45 }
46
47 #[test]
48 fn prompt_includes_issue_details() {
49 let prompt = build_prompt(&sample_issues(), &[]).unwrap();
50 assert!(prompt.contains("#1: Add login"));
51 assert!(prompt.contains("#2: Fix bug"));
52 assert!(prompt.contains("<issue_body>implement login flow</issue_body>"));
53 assert!(prompt.contains("<issue_body>crash on startup</issue_body>"));
54 }
55
56 #[test]
57 fn prompt_includes_complexity_classification() {
58 let prompt = build_prompt(&sample_issues(), &[]).unwrap();
59 assert!(prompt.contains("**simple**"));
60 assert!(prompt.contains("**full**"));
61 assert!(prompt.contains("Complexity Classification"));
62 }
63
64 #[test]
65 fn prompt_includes_dependency_analysis() {
66 let prompt = build_prompt(&sample_issues(), &[]).unwrap();
67 assert!(prompt.contains("Dependency Analysis"));
68 assert!(prompt.contains("depends_on"));
69 }
70
71 #[test]
72 fn prompt_structured_json_output_is_valid() {
73 let prompt = build_prompt(&sample_issues(), &[]).unwrap();
74 assert!(prompt.contains("\"complexity\": \"simple\""));
75 assert!(prompt.contains("\"has_migration\""));
76 assert!(prompt.contains("\"predicted_files\""));
77 assert!(prompt.contains("\"area\""));
78 assert!(prompt.contains("\"total_issues\""));
79 assert!(prompt.contains("\"parallel_capacity\""));
80 assert!(prompt.contains("\"depends_on\""));
81 assert!(prompt.contains("\"nodes\""));
82 }
83
84 #[test]
85 fn prompt_omits_graph_context_when_empty() {
86 let prompt = build_prompt(&sample_issues(), &[]).unwrap();
87 assert!(!prompt.contains("<graph_state>"));
88 }
89
90 #[test]
91 fn prompt_includes_graph_context() {
92 let graph_ctx = vec![
93 GraphContextNode {
94 number: 10,
95 title: "Refactor auth".to_string(),
96 state: crate::db::graph::NodeState::InFlight,
97 area: "auth".to_string(),
98 predicted_files: vec!["src/auth.rs".to_string(), "src/middleware.rs".to_string()],
99 has_migration: false,
100 depends_on: vec![],
101 target_repo: None,
102 },
103 GraphContextNode {
104 number: 11,
105 title: "Add migration".to_string(),
106 state: crate::db::graph::NodeState::AwaitingMerge,
107 area: "db".to_string(),
108 predicted_files: vec!["src/db/mod.rs".to_string()],
109 has_migration: true,
110 depends_on: vec![10],
111 target_repo: Some("backend".to_string()),
112 },
113 ];
114
115 let prompt = build_prompt(&sample_issues(), &graph_ctx).unwrap();
116 assert!(prompt.contains("<graph_state>"));
117 assert!(prompt.contains("#10: Refactor auth"));
118 assert!(prompt.contains("state: in_flight"));
119 assert!(prompt.contains("area: auth"));
120 assert!(prompt.contains("src/auth.rs"));
121 assert!(prompt.contains("src/middleware.rs"));
122 assert!(prompt.contains("has_migration: false"));
123 assert!(prompt.contains("#11: Add migration"));
124 assert!(prompt.contains("state: awaiting_merge"));
125 assert!(prompt.contains("has_migration: true"));
126 assert!(prompt.contains("#10"));
127 assert!(prompt.contains("target_repo: backend"));
129 let auth_section =
131 prompt.split("#10: Refactor auth").nth(1).unwrap().split('#').next().unwrap();
132 assert!(!auth_section.contains("target_repo"));
133 }
134}