use crate::claude::run_improve_session;
use crate::error::{Autom8Error, Result};
use crate::gh::find_spec_for_branch;
use crate::git::{self, CommitInfo, DiffEntry};
use crate::knowledge::ProjectKnowledge;
use crate::output::improve::print_context_summary;
use crate::spec::Spec;
use crate::state::StateManager;
use std::path::PathBuf;
pub fn improve_command(_verbose: bool) -> Result<()> {
if !git::is_git_repo() {
return Err(Autom8Error::NotInGitRepo);
}
let context = load_follow_up_context()?;
print_context_summary(&context);
let prompt = build_improve_prompt(&context);
run_improve_session(&prompt)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct GitContext {
pub branch_name: String,
pub base_branch: String,
pub commits: Vec<CommitInfo>,
pub diff_entries: Vec<DiffEntry>,
pub merge_base_commit: Option<String>,
}
impl GitContext {
pub fn is_feature_branch(&self) -> bool {
self.branch_name != "main" && self.branch_name != "master"
}
pub fn commit_count(&self) -> usize {
self.commits.len()
}
pub fn files_changed_count(&self) -> usize {
self.diff_entries.len()
}
pub fn total_additions(&self) -> u32 {
self.diff_entries.iter().map(|e| e.additions).sum()
}
pub fn total_deletions(&self) -> u32 {
self.diff_entries.iter().map(|e| e.deletions).sum()
}
}
pub fn gather_git_context() -> Result<GitContext> {
let branch_name = git::current_branch()?;
let base_branch = git::detect_base_branch().unwrap_or_else(|_| "main".to_string());
let merge_base_commit = git::get_merge_base(&base_branch).ok();
let commits = git::get_branch_commits(&base_branch).unwrap_or_default();
let diff_entries = if let Some(ref merge_base) = merge_base_commit {
git::get_diff_since(merge_base).unwrap_or_default()
} else {
git::get_diff_since(&base_branch).unwrap_or_default()
};
Ok(GitContext {
branch_name,
base_branch,
commits,
diff_entries,
merge_base_commit,
})
}
pub fn build_improve_prompt(context: &FollowUpContext) -> String {
let mut sections: Vec<String> = Vec::new();
let opening = build_opening_statement(context);
sections.push(opening);
if let Some(ref spec) = context.spec {
sections.push(build_spec_summary(spec));
}
if let Some(ref knowledge) = context.knowledge {
if !knowledge.decisions.is_empty() {
sections.push(build_decisions_summary(&knowledge.decisions));
}
}
if let Some(ref knowledge) = context.knowledge {
if !knowledge.story_changes.is_empty() {
if let Some(files_section) = build_files_summary(&knowledge.story_changes) {
sections.push(files_section);
}
}
}
if !context.work_summaries.is_empty() {
sections.push(build_work_summaries(&context.work_summaries));
}
sections.push("What would you like to work on?".to_string());
sections.join("\n\n")
}
fn build_opening_statement(context: &FollowUpContext) -> String {
let branch = &context.git.branch_name;
let level = context.richness_level();
match level {
3 => format!(
"You're on branch `{}`. I've loaded the spec, session knowledge, and git history.",
branch
),
2 => {
if context.has_spec() {
format!(
"You're on branch `{}`. I've loaded the spec and git history.",
branch
)
} else {
format!(
"You're on branch `{}`. I've loaded session knowledge and git history.",
branch
)
}
}
_ => format!(
"You're on branch `{}`. I've loaded the git history.",
branch
),
}
}
fn build_spec_summary(spec: &Spec) -> String {
let (completed, total) = spec.progress();
let status = if spec.all_complete() {
"all complete".to_string()
} else {
format!("{}/{} stories complete", completed, total)
};
format!("**Feature:** {} ({})", spec.project, status)
}
fn build_decisions_summary(decisions: &[crate::knowledge::Decision]) -> String {
let mut lines = vec!["**Key decisions:**".to_string()];
for decision in decisions.iter().take(5) {
lines.push(format!("- {}: {}", decision.topic, decision.choice));
}
if decisions.len() > 5 {
lines.push(format!("- ...and {} more", decisions.len() - 5));
}
lines.join("\n")
}
fn build_files_summary(story_changes: &[crate::knowledge::StoryChanges]) -> Option<String> {
use std::collections::HashSet;
let mut created: HashSet<&std::path::Path> = HashSet::new();
let mut modified: HashSet<&std::path::Path> = HashSet::new();
for changes in story_changes {
for file in &changes.files_created {
created.insert(&file.path);
}
for file in &changes.files_modified {
if !created.contains(file.path.as_path()) {
modified.insert(&file.path);
}
}
}
if created.is_empty() && modified.is_empty() {
return None;
}
let mut lines = vec!["**Files touched:**".to_string()];
let mut created_vec: Vec<_> = created.iter().collect();
created_vec.sort();
let mut modified_vec: Vec<_> = modified.iter().collect();
modified_vec.sort();
if !created_vec.is_empty() {
lines.push("Created:".to_string());
for path in created_vec.iter().take(8) {
lines.push(format!("- {}", path.display()));
}
if created_vec.len() > 8 {
lines.push(format!("- ...and {} more", created_vec.len() - 8));
}
}
if !modified_vec.is_empty() {
lines.push("Modified:".to_string());
for path in modified_vec.iter().take(8) {
lines.push(format!("- {}", path.display()));
}
if modified_vec.len() > 8 {
lines.push(format!("- ...and {} more", modified_vec.len() - 8));
}
}
Some(lines.join("\n"))
}
fn build_work_summaries(summaries: &[String]) -> String {
let mut lines = vec!["**Work completed:**".to_string()];
for summary in summaries.iter().take(5) {
let truncated = if summary.len() > 100 {
format!("{}...", &summary[..97])
} else {
summary.clone()
};
lines.push(format!("- {}", truncated));
}
if summaries.len() > 5 {
lines.push(format!("- ...and {} more iterations", summaries.len() - 5));
}
lines.join("\n")
}
#[derive(Debug, Clone)]
pub struct FollowUpContext {
pub git: GitContext,
pub spec: Option<Spec>,
pub spec_path: Option<PathBuf>,
pub knowledge: Option<ProjectKnowledge>,
pub work_summaries: Vec<String>,
pub session_id: Option<String>,
}
impl FollowUpContext {
pub fn has_spec(&self) -> bool {
self.spec.is_some()
}
pub fn has_knowledge(&self) -> bool {
self.knowledge.is_some()
}
pub fn has_work_summaries(&self) -> bool {
!self.work_summaries.is_empty()
}
pub fn work_summary_count(&self) -> usize {
self.work_summaries.len()
}
pub fn richness_level(&self) -> u8 {
let mut level = 1; if self.has_spec() {
level += 1;
}
if self.has_knowledge() {
level += 1;
}
level
}
}
pub fn load_follow_up_context() -> Result<FollowUpContext> {
let git = gather_git_context()?;
let state_manager = StateManager::new()?;
let session_metadata = state_manager
.find_session_for_branch(&git.branch_name)
.ok()
.flatten();
let mut spec: Option<Spec> = None;
let mut spec_path: Option<PathBuf> = None;
let mut knowledge: Option<ProjectKnowledge> = None;
let mut work_summaries: Vec<String> = Vec::new();
let mut session_id: Option<String> = None;
if let Some(ref metadata) = session_metadata {
session_id = Some(metadata.session_id.clone());
if let Some(ref path) = metadata.spec_json_path {
if path.exists() {
if let Ok(loaded_spec) = Spec::load(path) {
spec = Some(loaded_spec);
spec_path = Some(path.clone());
}
}
}
let session_state_manager = StateManager::with_session(metadata.session_id.clone())?;
if let Ok(Some(run_state)) = session_state_manager.load_current() {
knowledge = Some(run_state.knowledge.clone());
work_summaries = run_state
.iterations
.iter()
.filter_map(|iter| iter.work_summary.clone())
.collect();
}
}
if spec.is_none() {
if let Ok(Some((found_spec, found_path))) = find_spec_for_branch(&git.branch_name) {
spec = Some(found_spec);
spec_path = Some(found_path);
}
}
Ok(FollowUpContext {
git,
spec,
spec_path,
knowledge,
work_summaries,
session_id,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::{DiffEntry, DiffStatus};
use crate::knowledge::{Decision, Pattern};
use crate::spec::UserStory;
use std::path::PathBuf;
#[test]
fn test_git_context_is_feature_branch_true() {
let context = GitContext {
branch_name: "feature/improve-command".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
};
assert!(context.is_feature_branch());
}
#[test]
fn test_git_context_is_feature_branch_false_main() {
let context = GitContext {
branch_name: "main".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
};
assert!(!context.is_feature_branch());
}
#[test]
fn test_git_context_is_feature_branch_false_master() {
let context = GitContext {
branch_name: "master".to_string(),
base_branch: "master".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
};
assert!(!context.is_feature_branch());
}
#[test]
fn test_git_context_commit_count() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![
crate::git::CommitInfo {
short_hash: "abc1234".to_string(),
full_hash: "abc1234567890".to_string(),
message: "First commit".to_string(),
author: "Test".to_string(),
date: "2024-01-15".to_string(),
},
crate::git::CommitInfo {
short_hash: "def5678".to_string(),
full_hash: "def5678901234".to_string(),
message: "Second commit".to_string(),
author: "Test".to_string(),
date: "2024-01-16".to_string(),
},
],
diff_entries: vec![],
merge_base_commit: Some("basehash".to_string()),
};
assert_eq!(context.commit_count(), 2);
}
#[test]
fn test_git_context_files_changed_count() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![
DiffEntry {
path: PathBuf::from("src/lib.rs"),
additions: 10,
deletions: 5,
status: DiffStatus::Modified,
},
DiffEntry {
path: PathBuf::from("src/main.rs"),
additions: 20,
deletions: 0,
status: DiffStatus::Added,
},
],
merge_base_commit: None,
};
assert_eq!(context.files_changed_count(), 2);
}
#[test]
fn test_git_context_total_additions() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![
DiffEntry {
path: PathBuf::from("src/lib.rs"),
additions: 10,
deletions: 5,
status: DiffStatus::Modified,
},
DiffEntry {
path: PathBuf::from("src/main.rs"),
additions: 20,
deletions: 3,
status: DiffStatus::Modified,
},
],
merge_base_commit: None,
};
assert_eq!(context.total_additions(), 30);
}
#[test]
fn test_git_context_total_deletions() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![
DiffEntry {
path: PathBuf::from("src/lib.rs"),
additions: 10,
deletions: 5,
status: DiffStatus::Modified,
},
DiffEntry {
path: PathBuf::from("src/main.rs"),
additions: 20,
deletions: 3,
status: DiffStatus::Modified,
},
],
merge_base_commit: None,
};
assert_eq!(context.total_deletions(), 8);
}
#[test]
fn test_git_context_empty() {
let context = GitContext {
branch_name: "feature/empty".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
};
assert_eq!(context.commit_count(), 0);
assert_eq!(context.files_changed_count(), 0);
assert_eq!(context.total_additions(), 0);
assert_eq!(context.total_deletions(), 0);
}
#[test]
fn test_git_context_clone() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: Some("abc123".to_string()),
};
let cloned = context.clone();
assert_eq!(cloned.branch_name, context.branch_name);
assert_eq!(cloned.merge_base_commit, context.merge_base_commit);
}
#[test]
fn test_git_context_debug() {
let context = GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
};
let debug = format!("{:?}", context);
assert!(debug.contains("GitContext"));
assert!(debug.contains("feature/test"));
}
#[test]
fn test_gather_git_context_returns_valid_context() {
let result = gather_git_context();
assert!(result.is_ok());
let context = result.unwrap();
assert!(!context.branch_name.is_empty());
assert!(!context.base_branch.is_empty());
}
#[test]
fn test_gather_git_context_has_base_branch() {
let result = gather_git_context();
assert!(result.is_ok());
let context = result.unwrap();
assert!(
context.base_branch == "main" || context.base_branch == "master",
"Expected 'main' or 'master', got '{}'",
context.base_branch
);
}
fn make_git_context() -> GitContext {
GitContext {
branch_name: "feature/test".to_string(),
base_branch: "main".to_string(),
commits: vec![],
diff_entries: vec![],
merge_base_commit: None,
}
}
fn make_spec() -> Spec {
Spec {
project: "TestProject".to_string(),
branch_name: "feature/test".to_string(),
description: "Test description".to_string(),
user_stories: vec![UserStory {
id: "US-001".to_string(),
title: "Test Story".to_string(),
description: "Test".to_string(),
acceptance_criteria: vec![],
priority: 1,
passes: false,
notes: String::new(),
}],
}
}
fn make_knowledge() -> ProjectKnowledge {
let mut knowledge = ProjectKnowledge::default();
knowledge.decisions.push(Decision {
story_id: "US-001".to_string(),
topic: "Architecture".to_string(),
choice: "Use modules".to_string(),
rationale: "Better organization".to_string(),
});
knowledge.patterns.push(Pattern {
story_id: "US-001".to_string(),
description: "Use Result for errors".to_string(),
example_file: None,
});
knowledge
}
#[test]
fn test_follow_up_context_git_only() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
assert!(!context.has_spec());
assert!(!context.has_knowledge());
assert!(!context.has_work_summaries());
assert_eq!(context.richness_level(), 1);
}
#[test]
fn test_follow_up_context_with_spec() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: Some(PathBuf::from("/path/to/spec.json")),
knowledge: None,
work_summaries: vec![],
session_id: None,
};
assert!(context.has_spec());
assert!(!context.has_knowledge());
assert_eq!(context.richness_level(), 2);
}
#[test]
fn test_follow_up_context_full() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: Some(PathBuf::from("/path/to/spec.json")),
knowledge: Some(make_knowledge()),
work_summaries: vec![
"Implemented feature A".to_string(),
"Fixed bug in module B".to_string(),
],
session_id: Some("session-123".to_string()),
};
assert!(context.has_spec());
assert!(context.has_knowledge());
assert!(context.has_work_summaries());
assert_eq!(context.work_summary_count(), 2);
assert_eq!(context.richness_level(), 3);
}
#[test]
fn test_follow_up_context_richness_level_spec_only() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
assert_eq!(context.richness_level(), 2);
}
#[test]
fn test_follow_up_context_richness_level_knowledge_only() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(make_knowledge()),
work_summaries: vec![],
session_id: None,
};
assert_eq!(context.richness_level(), 2);
}
#[test]
fn test_follow_up_context_work_summaries_empty() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
assert!(!context.has_work_summaries());
assert_eq!(context.work_summary_count(), 0);
}
#[test]
fn test_follow_up_context_work_summaries_with_entries() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![
"Summary 1".to_string(),
"Summary 2".to_string(),
"Summary 3".to_string(),
],
session_id: None,
};
assert!(context.has_work_summaries());
assert_eq!(context.work_summary_count(), 3);
}
#[test]
fn test_follow_up_context_clone() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: Some(PathBuf::from("/path/to/spec.json")),
knowledge: Some(make_knowledge()),
work_summaries: vec!["Summary".to_string()],
session_id: Some("session-id".to_string()),
};
let cloned = context.clone();
assert_eq!(cloned.git.branch_name, context.git.branch_name);
assert_eq!(cloned.spec_path, context.spec_path);
assert_eq!(cloned.work_summaries.len(), context.work_summaries.len());
assert_eq!(cloned.session_id, context.session_id);
}
#[test]
fn test_follow_up_context_debug() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let debug = format!("{:?}", context);
assert!(debug.contains("FollowUpContext"));
assert!(debug.contains("git"));
}
use crate::knowledge::{FileChange, StoryChanges};
fn make_knowledge_with_files() -> ProjectKnowledge {
let mut knowledge = ProjectKnowledge::default();
knowledge.decisions.push(Decision {
story_id: "US-001".to_string(),
topic: "Architecture".to_string(),
choice: "Use modules".to_string(),
rationale: "Better organization".to_string(),
});
knowledge.decisions.push(Decision {
story_id: "US-002".to_string(),
topic: "Error handling".to_string(),
choice: "Use thiserror".to_string(),
rationale: "Clean error types".to_string(),
});
knowledge.story_changes.push(StoryChanges {
story_id: "US-001".to_string(),
files_created: vec![FileChange {
path: PathBuf::from("src/new_module.rs"),
additions: 100,
deletions: 0,
purpose: Some("New module".to_string()),
key_symbols: vec![],
}],
files_modified: vec![FileChange {
path: PathBuf::from("src/lib.rs"),
additions: 5,
deletions: 0,
purpose: None,
key_symbols: vec![],
}],
files_deleted: vec![],
commit_hash: None,
});
knowledge
}
fn make_complete_spec() -> Spec {
Spec {
project: "TestProject".to_string(),
branch_name: "feature/test".to_string(),
description: "Test description".to_string(),
user_stories: vec![
UserStory {
id: "US-001".to_string(),
title: "Test Story".to_string(),
description: "Test".to_string(),
acceptance_criteria: vec![],
priority: 1,
passes: true,
notes: String::new(),
},
UserStory {
id: "US-002".to_string(),
title: "Test Story 2".to_string(),
description: "Test 2".to_string(),
acceptance_criteria: vec![],
priority: 2,
passes: true,
notes: String::new(),
},
],
}
}
#[test]
fn test_build_improve_prompt_git_only() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("feature/test"));
assert!(prompt.contains("git history"));
assert!(prompt.contains("What would you like to work on?"));
assert!(!prompt.contains("**Feature:**"));
assert!(!prompt.contains("**Key decisions:**"));
}
#[test]
fn test_build_improve_prompt_with_spec() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("feature/test"));
assert!(prompt.contains("**Feature:**"));
assert!(prompt.contains("TestProject"));
assert!(prompt.contains("0/1 stories complete"));
assert!(prompt.contains("What would you like to work on?"));
}
#[test]
fn test_build_improve_prompt_with_complete_spec() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_complete_spec()),
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("all complete"));
}
#[test]
fn test_build_improve_prompt_with_decisions() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(make_knowledge_with_files()),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("**Key decisions:**"));
assert!(prompt.contains("Architecture: Use modules"));
assert!(prompt.contains("Error handling: Use thiserror"));
}
#[test]
fn test_build_improve_prompt_with_files() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(make_knowledge_with_files()),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("**Files touched:**"));
assert!(prompt.contains("Created:"));
assert!(prompt.contains("src/new_module.rs"));
assert!(prompt.contains("Modified:"));
assert!(prompt.contains("src/lib.rs"));
}
#[test]
fn test_build_improve_prompt_with_work_summaries() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![
"Implemented user authentication".to_string(),
"Fixed login validation bug".to_string(),
],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("**Work completed:**"));
assert!(prompt.contains("Implemented user authentication"));
assert!(prompt.contains("Fixed login validation bug"));
}
#[test]
fn test_build_improve_prompt_full_context() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: None,
knowledge: Some(make_knowledge_with_files()),
work_summaries: vec!["Completed initial setup".to_string()],
session_id: Some("session-123".to_string()),
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("spec, session knowledge, and git history"));
assert!(prompt.contains("**Feature:**"));
assert!(prompt.contains("**Key decisions:**"));
assert!(prompt.contains("**Files touched:**"));
assert!(prompt.contains("**Work completed:**"));
assert!(prompt.ends_with("What would you like to work on?"));
}
#[test]
fn test_build_improve_prompt_limits_decisions() {
let mut knowledge = ProjectKnowledge::default();
for i in 1..=7 {
knowledge.decisions.push(Decision {
story_id: format!("US-{:03}", i),
topic: format!("Topic {}", i),
choice: format!("Choice {}", i),
rationale: "Rationale".to_string(),
});
}
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(knowledge),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("Topic 1: Choice 1"));
assert!(prompt.contains("Topic 5: Choice 5"));
assert!(prompt.contains("...and 2 more"));
assert!(!prompt.contains("Topic 6: Choice 6"));
}
#[test]
fn test_build_improve_prompt_truncates_long_summaries() {
let long_summary = "A".repeat(150);
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![long_summary],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("..."));
assert!(!prompt.contains(&"A".repeat(150)));
}
#[test]
fn test_build_improve_prompt_limits_work_summaries() {
let summaries: Vec<String> = (1..=8).map(|i| format!("Summary {}", i)).collect();
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: summaries,
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("Summary 1"));
assert!(prompt.contains("Summary 5"));
assert!(prompt.contains("...and 3 more iterations"));
}
#[test]
fn test_build_improve_prompt_empty_knowledge_no_files_section() {
let mut knowledge = ProjectKnowledge::default();
knowledge.decisions.push(Decision {
story_id: "US-001".to_string(),
topic: "Test".to_string(),
choice: "Test".to_string(),
rationale: "Test".to_string(),
});
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(knowledge),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("**Key decisions:**"));
assert!(!prompt.contains("**Files touched:**"));
}
#[test]
fn test_build_improve_prompt_opening_level_1() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("I've loaded the git history"));
}
#[test]
fn test_build_improve_prompt_opening_level_2_with_spec() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: None,
knowledge: None,
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("I've loaded the spec and git history"));
}
#[test]
fn test_build_improve_prompt_opening_level_2_with_knowledge() {
let context = FollowUpContext {
git: make_git_context(),
spec: None,
spec_path: None,
knowledge: Some(make_knowledge()),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("I've loaded session knowledge and git history"));
}
#[test]
fn test_build_improve_prompt_opening_level_3() {
let context = FollowUpContext {
git: make_git_context(),
spec: Some(make_spec()),
spec_path: None,
knowledge: Some(make_knowledge()),
work_summaries: vec![],
session_id: None,
};
let prompt = build_improve_prompt(&context);
assert!(prompt.contains("I've loaded the spec, session knowledge, and git history"));
}
#[test]
fn test_load_follow_up_context_succeeds() {
let result = load_follow_up_context();
assert!(result.is_ok());
let context = result.unwrap();
assert!(!context.git.branch_name.is_empty());
assert!(context.richness_level() >= 1);
}
#[test]
fn test_load_follow_up_context_git_always_present() {
let result = load_follow_up_context();
assert!(result.is_ok());
let context = result.unwrap();
assert!(!context.git.branch_name.is_empty());
assert!(!context.git.base_branch.is_empty());
let _ = context.git.commit_count();
let _ = context.git.files_changed_count();
}
}