1mod commit;
18mod developer;
19pub mod partials;
20mod rebase;
21pub mod reviewer;
22pub mod template_catalog;
23pub mod template_context;
24mod template_engine;
25mod template_macros;
26pub mod template_registry;
27mod template_validator;
28mod types;
29
30pub use crate::checkpoint::restore::ResumeContext;
32
33pub use commit::{
35 prompt_fix_with_context, prompt_generate_commit_message_with_diff_with_context,
36 prompt_simplified_commit_with_context, prompt_xsd_retry_with_context,
37};
38pub use developer::{prompt_developer_iteration_with_context, prompt_plan_with_context};
39pub use rebase::{
40 build_conflict_resolution_prompt_with_context, collect_conflict_info, FileConflict,
41};
42
43#[cfg(any(test, feature = "test-utils"))]
44pub use rebase::build_enhanced_conflict_resolution_prompt;
45
46#[cfg(any(test, feature = "test-utils"))]
48pub use rebase::{collect_branch_info, BranchInfo};
49pub use reviewer::{
50 prompt_comprehensive_review_with_diff_with_context,
51 prompt_detailed_review_without_guidelines_with_diff_with_context,
52 prompt_incremental_review_with_diff_with_context,
53 prompt_reviewer_review_with_guidelines_and_diff_with_context,
54 prompt_security_focused_review_with_diff_with_context,
55 prompt_universal_review_with_diff_with_context,
56};
57
58#[cfg(test)]
60pub use commit::{prompt_fix, prompt_generate_commit_message_with_diff};
61#[cfg(test)]
62pub use developer::{prompt_developer_iteration, prompt_plan};
63pub use template_context::TemplateContext;
64pub use template_engine::Template;
65pub use template_validator::{
66 extract_metadata, extract_partials, extract_variables, validate_template, ValidationError,
67 ValidationWarning,
68};
69pub use types::{Action, ContextLevel, Role};
70
71#[derive(Debug, Clone, Default, PartialEq, Eq)]
75#[must_use]
76pub struct PromptConfig {
77 pub iteration: Option<u32>,
79 pub total_iterations: Option<u32>,
81 pub prompt_md_content: Option<String>,
83 pub prompt_and_plan: Option<(String, String)>,
85 pub prompt_plan_and_issues: Option<(String, String, String)>,
87 pub is_resume: bool,
89 pub resume_context: Option<ResumeContext>,
91}
92
93impl PromptConfig {
94 #[must_use = "configuration is required for prompt generation"]
96 pub const fn new() -> Self {
97 Self {
98 iteration: None,
99 total_iterations: None,
100 prompt_md_content: None,
101 prompt_and_plan: None,
102 prompt_plan_and_issues: None,
103 is_resume: false,
104 resume_context: None,
105 }
106 }
107
108 #[must_use = "returns the updated configuration for chaining"]
110 pub const fn with_iterations(mut self, iteration: u32, total: u32) -> Self {
111 self.iteration = Some(iteration);
112 self.total_iterations = Some(total);
113 self
114 }
115
116 #[must_use = "returns the updated configuration for chaining"]
118 pub fn with_prompt_md(mut self, content: String) -> Self {
119 self.prompt_md_content = Some(content);
120 self
121 }
122
123 #[must_use = "returns the updated configuration for chaining"]
125 pub fn with_prompt_and_plan(mut self, prompt: String, plan: String) -> Self {
126 self.prompt_and_plan = Some((prompt, plan));
127 self
128 }
129
130 pub fn with_prompt_plan_and_issues(
132 mut self,
133 prompt: String,
134 plan: String,
135 issues: String,
136 ) -> Self {
137 self.prompt_plan_and_issues = Some((prompt, plan, issues));
138 self
139 }
140
141 #[cfg(test)]
143 #[must_use = "returns the updated configuration for chaining"]
144 pub const fn with_resume(mut self, is_resume: bool) -> Self {
145 self.is_resume = is_resume;
146 self
147 }
148
149 #[must_use = "returns the updated configuration for chaining"]
151 pub fn with_resume_context(mut self, context: ResumeContext) -> Self {
152 self.resume_context = Some(context);
153 self.is_resume = true;
154 self
155 }
156}
157
158pub fn generate_resume_note(context: &ResumeContext) -> String {
169 let mut note = String::from("SESSION RESUME CONTEXT\n");
170 note.push_str("====================\n\n");
171
172 match context.phase {
174 crate::checkpoint::state::PipelinePhase::Development => {
175 note.push_str(&format!(
176 "Resuming DEVELOPMENT phase (iteration {} of {})\n",
177 context.iteration + 1,
178 context.total_iterations
179 ));
180 }
181 crate::checkpoint::state::PipelinePhase::Review => {
182 note.push_str(&format!(
183 "Resuming REVIEW phase (pass {} of {})\n",
184 context.reviewer_pass + 1,
185 context.total_reviewer_passes
186 ));
187 }
188 crate::checkpoint::state::PipelinePhase::ReviewAgain => {
189 note.push_str(&format!(
190 "Resuming VERIFICATION REVIEW phase (pass {} of {})\n",
191 context.reviewer_pass + 1,
192 context.total_reviewer_passes
193 ));
194 }
195 crate::checkpoint::state::PipelinePhase::Fix => {
196 note.push_str("Resuming FIX phase\n");
197 }
198 _ => {
199 note.push_str(&format!("Resuming from phase: {}\n", context.phase_name()));
200 }
201 }
202
203 if context.resume_count > 0 {
205 note.push_str(&format!(
206 "This session has been resumed {} time(s)\n",
207 context.resume_count
208 ));
209 }
210
211 if !matches!(
213 context.rebase_state,
214 crate::checkpoint::state::RebaseState::NotStarted
215 ) {
216 note.push_str(&format!("Rebase state: {:?}\n", context.rebase_state));
217 }
218
219 note.push('\n');
220
221 if let Some(ref history) = context.execution_history {
223 if !history.steps.is_empty() {
224 note.push_str("RECENT ACTIVITY:\n");
225 note.push_str("----------------\n");
226
227 let recent_steps: Vec<_> = history
229 .steps
230 .iter()
231 .rev()
232 .take(5)
233 .collect::<Vec<_>>()
234 .into_iter()
235 .rev()
236 .collect();
237
238 for step in &recent_steps {
239 note.push_str(&format!(
240 "- [{}] {} (iteration {}): {}\n",
241 step.step_type,
242 step.phase,
243 step.iteration,
244 step.outcome.brief_description()
245 ));
246
247 if let Some(ref detail) = step.modified_files_detail {
249 let total_files =
250 detail.added.len() + detail.modified.len() + detail.deleted.len();
251 if total_files > 0 {
252 note.push_str(&format!(" Files: {} changed", total_files));
253 if !detail.added.is_empty() {
254 note.push_str(&format!(" ({} added)", detail.added.len()));
255 }
256 if !detail.modified.is_empty() {
257 note.push_str(&format!(" ({} modified)", detail.modified.len()));
258 }
259 if !detail.deleted.is_empty() {
260 note.push_str(&format!(" ({} deleted)", detail.deleted.len()));
261 }
262 note.push('\n');
263 }
264 }
265
266 if let Some(ref issues) = step.issues_summary {
268 if issues.found > 0 || issues.fixed > 0 {
269 note.push_str(&format!(
270 " Issues: {} found, {} fixed",
271 issues.found, issues.fixed
272 ));
273 if let Some(ref desc) = issues.description {
274 note.push_str(&format!(" ({})", desc));
275 }
276 note.push('\n');
277 }
278 }
279
280 if let Some(ref oid) = step.git_commit_oid {
282 note.push_str(&format!(" Commit: {}\n", oid));
283 }
284 }
285
286 note.push('\n');
287 }
288 }
289
290 note.push_str("Previous progress is preserved in git history.\n");
291 note.push_str("Check 'git log' for details about what was done before.\n");
292
293 note.push_str("\nGUIDANCE:\n");
295 note.push_str("--------\n");
296 match context.phase {
297 crate::checkpoint::state::PipelinePhase::Development => {
298 note.push_str("Continue working on the implementation tasks from your plan.\n");
299 }
300 crate::checkpoint::state::PipelinePhase::Review
301 | crate::checkpoint::state::PipelinePhase::ReviewAgain => {
302 note.push_str("Review the code changes and provide feedback.\n");
303 }
304 crate::checkpoint::state::PipelinePhase::Fix => {
305 note.push_str("Focus on addressing the issues identified in the review.\n");
306 }
307 _ => {}
308 }
309
310 note.push('\n');
311 note
312}
313
314trait BriefDescription {
316 fn brief_description(&self) -> String;
317}
318
319impl BriefDescription for crate::checkpoint::execution_history::StepOutcome {
320 fn brief_description(&self) -> String {
321 match self {
322 Self::Success {
323 files_modified,
324 output,
325 ..
326 } => {
327 if let Some(ref out) = output {
328 if !out.is_empty() {
329 format!("Success - {}", out.lines().next().unwrap_or(""))
330 } else if !files_modified.is_empty() {
331 format!("Success - {} files modified", files_modified.len())
332 } else {
333 "Success".to_string()
334 }
335 } else if !files_modified.is_empty() {
336 format!("Success - {} files modified", files_modified.len())
337 } else {
338 "Success".to_string()
339 }
340 }
341 Self::Failure {
342 error, recoverable, ..
343 } => {
344 if *recoverable {
345 format!("Recoverable error - {}", error.lines().next().unwrap_or(""))
346 } else {
347 format!("Failed - {}", error.lines().next().unwrap_or(""))
348 }
349 }
350 Self::Partial {
351 completed,
352 remaining,
353 ..
354 } => {
355 format!("Partial - {} done, {}", completed, remaining)
356 }
357 Self::Skipped { reason } => {
358 format!("Skipped - {}", reason)
359 }
360 }
361 }
362}
363
364pub fn prompt_for_agent(
382 role: Role,
383 action: Action,
384 context: ContextLevel,
385 template_context: &TemplateContext,
386 config: PromptConfig,
387) -> String {
388 let resume_note = if let Some(resume_ctx) = &config.resume_context {
389 generate_resume_note(resume_ctx)
390 } else if config.is_resume {
391 "\nNOTE: This session is resuming from a previous run. Previous progress is preserved in git history. You can check 'git log' for context about what was done before.\n\n".to_string()
393 } else {
394 String::new()
395 };
396
397 let base_prompt = match (role, action) {
398 (_, Action::Plan) => {
399 prompt_plan_with_context(template_context, config.prompt_md_content.as_deref())
400 }
401 (Role::Developer | Role::Reviewer, Action::Iterate) => {
402 let (prompt_content, plan_content) = config
403 .prompt_and_plan
404 .unwrap_or((String::new(), String::new()));
405 prompt_developer_iteration_with_context(
406 template_context,
407 config.iteration.unwrap_or(1),
408 config.total_iterations.unwrap_or(1),
409 context,
410 &prompt_content,
411 &plan_content,
412 )
413 }
414 (_, Action::Fix) => {
415 let (prompt_content, plan_content, issues_content) = config
416 .prompt_plan_and_issues
417 .unwrap_or((String::new(), String::new(), String::new()));
418 prompt_fix_with_context(
419 template_context,
420 &prompt_content,
421 &plan_content,
422 &issues_content,
423 )
424 }
425 };
426
427 if config.is_resume {
429 format!("{}{}", resume_note, base_prompt)
430 } else {
431 base_prompt
432 }
433}
434
435pub fn get_stored_or_generate_prompt<F>(
467 prompt_key: &str,
468 prompt_history: &std::collections::HashMap<String, String>,
469 generator: F,
470) -> (String, bool)
471where
472 F: FnOnce() -> String,
473{
474 if let Some(stored_prompt) = prompt_history.get(prompt_key) {
475 (stored_prompt.clone(), true)
476 } else {
477 (generator(), false)
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::prompts::template_context::TemplateContext;
485
486 use crate::prompts::reviewer::prompt_detailed_review_without_guidelines_with_diff;
488
489 #[test]
490 fn test_prompt_for_agent_developer() {
491 let template_context = TemplateContext::default();
492 let result = prompt_for_agent(
493 Role::Developer,
494 Action::Iterate,
495 ContextLevel::Normal,
496 &template_context,
497 PromptConfig::new()
498 .with_iterations(3, 10)
499 .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
500 );
501 assert!(!result.contains("PROMPT.md"));
503 assert!(result.contains("test prompt"));
504 assert!(result.contains("test plan"));
505 }
506
507 #[test]
508 fn test_prompt_for_agent_reviewer() {
509 let result = prompt_detailed_review_without_guidelines_with_diff(
510 ContextLevel::Minimal,
511 "sample diff",
512 "",
513 "",
514 );
515 assert!(result.contains("REVIEW MODE"));
518 assert!(result.contains("CRITICAL CONSTRAINTS"));
519 }
520
521 #[test]
522 fn test_prompt_for_agent_plan() {
523 let template_context = TemplateContext::default();
524 let result = prompt_for_agent(
525 Role::Developer,
526 Action::Plan,
527 ContextLevel::Normal,
528 &template_context,
529 PromptConfig::new().with_prompt_md("test requirements".to_string()),
530 );
531 assert!(result.contains("PLANNING MODE"));
533 assert!(result.contains("Implementation Steps"));
534 }
535
536 #[test]
537 fn test_prompts_are_agent_agnostic() {
538 let agent_specific_terms = [
541 "claude", "codex", "opencode", "gemini", "aider", "goose", "cline", "continue",
542 "amazon-q", "gpt", "copilot",
543 ];
544
545 let prompts_to_check: Vec<String> = vec![
546 prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
547 prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
548 prompt_detailed_review_without_guidelines_with_diff(
549 ContextLevel::Normal,
550 "sample diff",
551 "",
552 "",
553 ),
554 prompt_detailed_review_without_guidelines_with_diff(
555 ContextLevel::Minimal,
556 "sample diff",
557 "",
558 "",
559 ),
560 prompt_fix("", "", ""),
561 prompt_plan(None),
562 prompt_generate_commit_message_with_diff("diff --git a/a b/b"),
563 ];
564
565 for prompt in prompts_to_check {
566 let prompt_lower = prompt.to_lowercase();
567 for term in agent_specific_terms {
568 assert!(
569 !prompt_lower.contains(term),
570 "Prompt contains agent-specific term '{}': {}",
571 term,
572 &prompt[..prompt.len().min(100)]
573 );
574 }
575 }
576 }
577
578 #[test]
579 fn test_prompt_for_agent_fix() {
580 let template_context = TemplateContext::default();
581 let result = prompt_for_agent(
582 Role::Developer,
583 Action::Fix,
584 ContextLevel::Normal,
585 &template_context,
586 PromptConfig::new().with_prompt_plan_and_issues(
587 "test prompt".to_string(),
588 "test plan".to_string(),
589 "test issues".to_string(),
590 ),
591 );
592 assert!(result.contains("FIX MODE"));
593 assert!(result.contains("test issues"));
594 assert!(result.contains("test prompt"));
596 assert!(result.contains("test plan"));
597 }
598
599 #[test]
600 fn test_prompt_for_agent_fix_with_empty_context() {
601 let template_context = TemplateContext::default();
602 let result = prompt_for_agent(
603 Role::Developer,
604 Action::Fix,
605 ContextLevel::Normal,
606 &template_context,
607 PromptConfig::new(),
608 );
609 assert!(result.contains("FIX MODE"));
610 assert!(!result.is_empty());
612 }
613
614 #[test]
615 fn test_reviewer_can_use_iterate_action() {
616 let template_context = TemplateContext::default();
618 let result = prompt_for_agent(
619 Role::Reviewer,
620 Action::Iterate,
621 ContextLevel::Normal,
622 &template_context,
623 PromptConfig::new()
624 .with_iterations(1, 3)
625 .with_prompt_and_plan(String::new(), String::new()),
626 );
627 assert!(result.contains("IMPLEMENTATION MODE"));
629 }
630
631 #[test]
632 fn test_prompts_do_not_have_detailed_tracking_language() {
633 let detailed_tracking_terms = [
636 "iteration number",
637 "phase completed",
638 "previous iteration",
639 "history of",
640 "detailed log",
641 ];
642
643 let prompts_to_check = vec![
644 prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
645 prompt_fix("", "", ""),
646 ];
647
648 for prompt in prompts_to_check {
649 let prompt_lower = prompt.to_lowercase();
650 for term in detailed_tracking_terms {
651 assert!(
652 !prompt_lower.contains(term),
653 "Prompt contains detailed tracking language '{}': {}",
654 term,
655 &prompt[..prompt.len().min(100)]
656 );
657 }
658 }
659 }
660
661 #[test]
662 fn test_developer_notes_md_not_referenced() {
663 let developer_prompt = prompt_developer_iteration(1, 5, ContextLevel::Normal, "", "");
665 assert!(
666 !developer_prompt.contains("NOTES.md"),
667 "Developer prompt should not reference NOTES.md in isolation mode"
668 );
669 }
670
671 #[test]
672 fn test_all_prompts_isolate_agents_from_git() {
673 let instructive_git_patterns = [
680 "Run `git",
681 "run git",
682 "execute git",
683 "Try: git",
684 "you can git",
685 "should run git",
686 "please run git",
687 "\ngit ", ];
689
690 let forbid_contexts = [
693 "MUST NOT run",
694 "DO NOT run",
695 "must not run",
696 "do not run",
697 "NOT run commands",
698 "commands (",
699 "commands:",
700 "including:",
701 "such as",
702 ];
703
704 let prompts_to_check: Vec<String> = vec![
709 prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
710 prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
711 prompt_detailed_review_without_guidelines_with_diff(
712 ContextLevel::Normal,
713 "sample diff",
714 "",
715 "",
716 ),
717 prompt_detailed_review_without_guidelines_with_diff(
718 ContextLevel::Minimal,
719 "sample diff",
720 "",
721 "",
722 ),
723 prompt_fix("", "", ""),
727 prompt_plan(None),
728 prompt_generate_commit_message_with_diff("diff --git a/a b/b\n"),
729 ];
730
731 for prompt in prompts_to_check {
732 for pattern in instructive_git_patterns {
733 if prompt.contains(pattern) {
734 let is_forbidden = forbid_contexts.iter().any(|ctx| {
736 if let Some(pos) = prompt.find(ctx) {
737 if let Some(pattern_pos) = prompt[pos..].find(pattern) {
739 pattern_pos < 200
741 } else {
742 false
743 }
744 } else {
745 false
746 }
747 });
748
749 if !is_forbidden {
750 panic!(
751 "Prompt contains instructive git command pattern '{}': {}",
752 pattern,
753 &prompt[..prompt.len().min(150)]
754 );
755 }
756 }
757 }
758 }
759
760 let orchestrator_prompt = prompt_generate_commit_message_with_diff("some diff");
764 assert!(
765 orchestrator_prompt.contains("DIFF:") || orchestrator_prompt.contains("diff"),
766 "Orchestrator prompt should contain the diff content for commit message generation"
767 );
768 for pattern in instructive_git_patterns {
770 if orchestrator_prompt.contains(pattern) {
771 let is_forbidden = forbid_contexts.iter().any(|ctx| {
773 if let Some(pos) = orchestrator_prompt.find(ctx) {
774 if let Some(pattern_pos) = orchestrator_prompt[pos..].find(pattern) {
775 pattern_pos < 200
776 } else {
777 false
778 }
779 } else {
780 false
781 }
782 });
783
784 assert!(
785 is_forbidden,
786 "Orchestrator prompt contains instructive git command pattern '{pattern}'"
787 );
788 }
789 }
790 }
791
792 #[test]
793 fn test_prompt_with_resume_context() {
794 let template_context = TemplateContext::default();
795 let result = prompt_for_agent(
796 Role::Developer,
797 Action::Iterate,
798 ContextLevel::Normal,
799 &template_context,
800 PromptConfig::new()
801 .with_resume(true)
802 .with_iterations(2, 5)
803 .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
804 );
805 assert!(result.contains("resuming from a previous run"));
807 assert!(result.contains("git log"));
808 }
809
810 #[test]
811 fn test_prompt_with_rich_resume_context_development() {
812 use crate::checkpoint::state::{PipelinePhase, RebaseState};
813
814 let template_context = TemplateContext::default();
815
816 let resume_context = ResumeContext {
818 phase: PipelinePhase::Development,
819 iteration: 2,
820 total_iterations: 5,
821 reviewer_pass: 0,
822 total_reviewer_passes: 3,
823 resume_count: 1,
824 rebase_state: RebaseState::NotStarted,
825 run_id: "test-run-id".to_string(),
826 prompt_history: None,
827 execution_history: None,
828 };
829
830 let result = prompt_for_agent(
831 Role::Developer,
832 Action::Iterate,
833 ContextLevel::Normal,
834 &template_context,
835 PromptConfig::new()
836 .with_resume_context(resume_context)
837 .with_iterations(3, 5)
838 .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
839 );
840
841 assert!(result.contains("SESSION RESUME CONTEXT"));
843 assert!(result.contains("DEVELOPMENT phase"));
844 assert!(result.contains("iteration 3 of 5"));
845 assert!(result.contains("has been resumed 1 time"));
846 assert!(result.contains("Continue working on the implementation"));
847 }
848
849 #[test]
850 fn test_prompt_with_rich_resume_context_review() {
851 use crate::checkpoint::state::{PipelinePhase, RebaseState};
852
853 let template_context = TemplateContext::default();
854
855 let resume_context = ResumeContext {
857 phase: PipelinePhase::Review,
858 iteration: 5,
859 total_iterations: 5,
860 reviewer_pass: 1,
861 total_reviewer_passes: 3,
862 resume_count: 2,
863 rebase_state: RebaseState::NotStarted,
864 run_id: "test-run-id".to_string(),
865 prompt_history: None,
866 execution_history: None,
867 };
868
869 let result = prompt_for_agent(
870 Role::Reviewer,
871 Action::Fix,
872 ContextLevel::Normal,
873 &template_context,
874 PromptConfig::new()
875 .with_resume_context(resume_context)
876 .with_prompt_plan_and_issues(
877 "test prompt".to_string(),
878 "test plan".to_string(),
879 "test issues".to_string(),
880 ),
881 );
882
883 assert!(result.contains("SESSION RESUME CONTEXT"));
885 assert!(result.contains("REVIEW phase"));
886 assert!(result.contains("pass 2 of 3"));
887 assert!(result.contains("has been resumed 2 time"));
888 }
889
890 #[test]
891 fn test_prompt_with_rich_resume_context_fix() {
892 use crate::checkpoint::state::{PipelinePhase, RebaseState};
893
894 let template_context = TemplateContext::default();
895
896 let resume_context = ResumeContext {
898 phase: PipelinePhase::Fix,
899 iteration: 5,
900 total_iterations: 5,
901 reviewer_pass: 1,
902 total_reviewer_passes: 3,
903 resume_count: 0,
904 rebase_state: RebaseState::NotStarted,
905 run_id: "test-run-id".to_string(),
906 prompt_history: None,
907 execution_history: None,
908 };
909
910 let result = prompt_for_agent(
911 Role::Reviewer,
912 Action::Fix,
913 ContextLevel::Normal,
914 &template_context,
915 PromptConfig::new()
916 .with_resume_context(resume_context)
917 .with_prompt_plan_and_issues(
918 "test prompt".to_string(),
919 "test plan".to_string(),
920 "test issues".to_string(),
921 ),
922 );
923
924 assert!(result.contains("SESSION RESUME CONTEXT"));
926 assert!(result.contains("FIX phase"));
927 assert!(result.contains("Focus on addressing the issues"));
928 }
929
930 #[test]
931 fn test_get_stored_or_generate_prompt_replays_when_available() {
932 let mut history = std::collections::HashMap::new();
933 history.insert("test_key".to_string(), "stored prompt".to_string());
934
935 let (prompt, was_replayed) =
936 get_stored_or_generate_prompt("test_key", &history, || "generated prompt".to_string());
937
938 assert_eq!(prompt, "stored prompt");
939 assert!(was_replayed, "Should have replayed the stored prompt");
940 }
941
942 #[test]
943 fn test_get_stored_or_generate_prompt_generates_when_not_available() {
944 let history = std::collections::HashMap::new();
945
946 let (prompt, was_replayed) = get_stored_or_generate_prompt("missing_key", &history, || {
947 "generated prompt".to_string()
948 });
949
950 assert_eq!(prompt, "generated prompt");
951 assert!(!was_replayed, "Should have generated a new prompt");
952 }
953
954 #[test]
955 fn test_get_stored_or_generate_prompt_with_empty_history() {
956 let history = std::collections::HashMap::new();
957
958 let (prompt, was_replayed) =
959 get_stored_or_generate_prompt("any_key", &history, || "fresh prompt".to_string());
960
961 assert_eq!(prompt, "fresh prompt");
962 assert!(
963 !was_replayed,
964 "Should have generated a new prompt for empty history"
965 );
966 }
967}