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