use destructive_command_guard::suggest::{
AllowlistSuggestion, CommandEntryInfo, ConfidenceTier, RiskLevel, SuggestionReason,
filter_by_confidence, filter_by_risk, generate_enhanced_suggestions,
};
fn create_test_entries(
commands: &[(&str, &str, bool)],
count_per_cmd: usize,
) -> Vec<CommandEntryInfo> {
let mut entries = Vec::new();
for (cmd, dir, bypassed) in commands {
for _ in 0..count_per_cmd {
entries.push(CommandEntryInfo {
command: cmd.to_string(),
working_dir: dir.to_string(),
was_bypassed: *bypassed,
});
}
}
entries
}
#[test]
fn generate_enhanced_suggestions_empty_input() {
let entries: Vec<CommandEntryInfo> = vec![];
let suggestions = generate_enhanced_suggestions(&entries, 1);
assert!(suggestions.is_empty());
}
#[test]
fn generate_enhanced_suggestions_single_command_high_frequency() {
let entries = create_test_entries(
&[("git reset --hard HEAD", "/data/projects/myapp", false)],
15,
);
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(
!suggestions.is_empty(),
"Should generate suggestion for high-frequency command"
);
let suggestion = &suggestions[0];
assert_eq!(suggestion.confidence, ConfidenceTier::High);
assert_eq!(suggestion.risk, RiskLevel::High);
}
#[test]
fn generate_enhanced_suggestions_filters_by_min_frequency() {
let entries = create_test_entries(
&[
("git reset --hard HEAD", "/data/projects/app1", false),
("npm run build", "/data/projects/app2", false),
],
2, );
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(
suggestions.is_empty(),
"Commands below min_frequency should not generate suggestions"
);
}
#[test]
fn generate_enhanced_suggestions_with_bypasses() {
let entries: Vec<CommandEntryInfo> = vec![
CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: true,
},
CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: true,
},
CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
},
];
let suggestions = generate_enhanced_suggestions(&entries, 2);
assert!(!suggestions.is_empty());
let suggestion = &suggestions[0];
assert_eq!(suggestion.confidence, ConfidenceTier::High);
assert_eq!(suggestion.reason, SuggestionReason::ManuallyBypassed);
assert_eq!(suggestion.bypass_count, 2);
}
#[test]
fn generate_enhanced_suggestions_clustered_paths() {
let entries = create_test_entries(&[("git status", "/data/projects/myapp", false)], 10);
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(!suggestions.is_empty());
let suggestion = &suggestions[0];
assert!(suggestion.suggest_path_specific);
assert!(!suggestion.path_patterns.is_empty());
}
#[test]
fn generate_enhanced_suggestions_scattered_paths() {
let scattered_paths = [
"/home/user1/project",
"/var/www/app",
"/opt/services/backend",
"/tmp/builds/staging",
"/usr/local/src/tool",
"/data/apps/frontend",
"/srv/containers/api",
"/mnt/storage/repo",
"/root/workspace/lib",
"/etc/scripts/util",
];
let entries: Vec<CommandEntryInfo> = scattered_paths
.iter()
.map(|path| CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: path.to_string(),
was_bypassed: false,
})
.collect();
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(!suggestions.is_empty());
let suggestion = &suggestions[0];
assert!(!suggestion.suggest_path_specific);
}
#[test]
fn generate_enhanced_suggestions_multiple_commands_sorted_by_score() {
let mut entries = Vec::new();
for _ in 0..20 {
entries.push(CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: true,
});
}
for _ in 0..5 {
entries.push(CommandEntryInfo {
command: "rm -rf /tmp/cache".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
});
}
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(
suggestions.len() >= 2,
"Should generate suggestions for both commands"
);
for i in 1..suggestions.len() {
assert!(
suggestions[i - 1].score >= suggestions[i].score,
"Suggestions should be sorted by score descending"
);
}
}
fn create_suggestion_with_confidence(confidence: ConfidenceTier) -> AllowlistSuggestion {
use destructive_command_guard::suggest::CommandCluster;
let cluster = CommandCluster {
commands: vec!["test command".to_string()],
normalized: vec!["test command".to_string()],
proposed_pattern: "^test command$".to_string(),
frequency: match confidence {
ConfidenceTier::High => 20,
ConfidenceTier::Medium => 7,
ConfidenceTier::Low => 2,
},
unique_count: 1,
};
let mut suggestion = AllowlistSuggestion::from_cluster(cluster);
suggestion.confidence = confidence;
suggestion
}
fn create_suggestion_with_risk(risk: RiskLevel) -> AllowlistSuggestion {
use destructive_command_guard::suggest::CommandCluster;
let cmd = match risk {
RiskLevel::High => "rm -rf /tmp",
RiskLevel::Medium => "git checkout -- file",
RiskLevel::Low => "npm run build",
};
let cluster = CommandCluster {
commands: vec![cmd.to_string()],
normalized: vec![cmd.to_string()],
proposed_pattern: format!("^{}$", regex::escape(cmd)),
frequency: 10,
unique_count: 1,
};
AllowlistSuggestion::from_cluster(cluster)
}
#[test]
fn filter_by_confidence_high_only() {
let suggestions = vec![
create_suggestion_with_confidence(ConfidenceTier::High),
create_suggestion_with_confidence(ConfidenceTier::Medium),
create_suggestion_with_confidence(ConfidenceTier::Low),
];
let filtered = filter_by_confidence(suggestions.clone(), ConfidenceTier::High);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].confidence, ConfidenceTier::High);
}
#[test]
fn filter_by_confidence_medium_exact_match() {
let suggestions = vec![
create_suggestion_with_confidence(ConfidenceTier::High),
create_suggestion_with_confidence(ConfidenceTier::Medium),
create_suggestion_with_confidence(ConfidenceTier::Low),
];
let filtered = filter_by_confidence(suggestions.clone(), ConfidenceTier::Medium);
assert_eq!(filtered.len(), 1);
assert!(
filtered
.iter()
.all(|s| s.confidence == ConfidenceTier::Medium)
);
}
#[test]
fn filter_by_confidence_low_exact_match() {
let suggestions = vec![
create_suggestion_with_confidence(ConfidenceTier::High),
create_suggestion_with_confidence(ConfidenceTier::Medium),
create_suggestion_with_confidence(ConfidenceTier::Low),
];
let filtered = filter_by_confidence(suggestions.clone(), ConfidenceTier::Low);
assert_eq!(filtered.len(), 1);
assert!(filtered.iter().all(|s| s.confidence == ConfidenceTier::Low));
}
#[test]
fn filter_by_risk_low_only() {
let suggestions = vec![
create_suggestion_with_risk(RiskLevel::High),
create_suggestion_with_risk(RiskLevel::Medium),
create_suggestion_with_risk(RiskLevel::Low),
];
let filtered = filter_by_risk(suggestions.clone(), RiskLevel::Low);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].risk, RiskLevel::Low);
}
#[test]
fn filter_by_risk_medium_exact_match() {
let suggestions = vec![
create_suggestion_with_risk(RiskLevel::High),
create_suggestion_with_risk(RiskLevel::Medium),
create_suggestion_with_risk(RiskLevel::Low),
];
let filtered = filter_by_risk(suggestions.clone(), RiskLevel::Medium);
assert_eq!(filtered.len(), 1);
assert!(filtered.iter().all(|s| s.risk == RiskLevel::Medium));
}
#[test]
fn filter_by_risk_high_exact_match() {
let suggestions = vec![
create_suggestion_with_risk(RiskLevel::High),
create_suggestion_with_risk(RiskLevel::Medium),
create_suggestion_with_risk(RiskLevel::Low),
];
let filtered = filter_by_risk(suggestions.clone(), RiskLevel::High);
assert_eq!(filtered.len(), 1);
assert!(filtered.iter().all(|s| s.risk == RiskLevel::High));
}
#[test]
fn filter_chain_confidence_then_risk() {
let mut suggestions = Vec::new();
let mut s1 = create_suggestion_with_confidence(ConfidenceTier::High);
s1.risk = RiskLevel::High;
suggestions.push(s1);
let mut s2 = create_suggestion_with_confidence(ConfidenceTier::High);
s2.risk = RiskLevel::Low;
suggestions.push(s2);
let mut s3 = create_suggestion_with_confidence(ConfidenceTier::Low);
s3.risk = RiskLevel::Low;
suggestions.push(s3);
let after_confidence = filter_by_confidence(suggestions, ConfidenceTier::High);
assert_eq!(after_confidence.len(), 2);
let after_risk = filter_by_risk(after_confidence, RiskLevel::Low);
assert_eq!(after_risk.len(), 1);
assert_eq!(after_risk[0].confidence, ConfidenceTier::High);
assert_eq!(after_risk[0].risk, RiskLevel::Low);
}
#[test]
fn generate_enhanced_suggestions_with_duplicate_commands() {
let entries: Vec<CommandEntryInfo> = (0..10)
.map(|_| CommandEntryInfo {
command: "git status".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
})
.collect();
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].cluster.frequency, 10);
}
#[test]
fn generate_enhanced_suggestions_with_similar_commands() {
let mut entries = Vec::new();
for _ in 0..5 {
entries.push(CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
});
entries.push(CommandEntryInfo {
command: "npm run test".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
});
}
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(!suggestions.is_empty());
}
#[test]
fn generate_enhanced_suggestions_preserves_command_order_in_cluster() {
let entries: Vec<CommandEntryInfo> = vec![
CommandEntryInfo {
command: "git reset --hard HEAD".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
},
CommandEntryInfo {
command: "git reset --hard HEAD".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
},
CommandEntryInfo {
command: "git reset --hard HEAD".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
},
];
let suggestions = generate_enhanced_suggestions(&entries, 2);
assert!(!suggestions.is_empty());
assert!(
suggestions[0]
.cluster
.commands
.contains(&"git reset --hard HEAD".to_string())
);
}
#[test]
fn scenario_ci_build_commands() {
let mut entries = Vec::new();
for _ in 0..50 {
entries.push(CommandEntryInfo {
command: "npm run build".to_string(),
working_dir: "/workspace/frontend".to_string(),
was_bypassed: false,
});
}
for _ in 0..30 {
entries.push(CommandEntryInfo {
command: "npm run test".to_string(),
working_dir: "/workspace/frontend".to_string(),
was_bypassed: false,
});
}
let suggestions = generate_enhanced_suggestions(&entries, 10);
assert!(suggestions.len() >= 1);
assert!(
suggestions
.iter()
.all(|s| s.confidence == ConfidenceTier::High)
);
assert!(suggestions.iter().all(|s| s.risk == RiskLevel::Low));
}
#[test]
fn scenario_developer_workflow() {
let mut entries = Vec::new();
for i in 0..8 {
entries.push(CommandEntryInfo {
command: "git reset --hard HEAD~1".to_string(),
working_dir: "/home/dev/myproject".to_string(),
was_bypassed: i >= 5, });
}
let suggestions = generate_enhanced_suggestions(&entries, 3);
assert!(!suggestions.is_empty());
let suggestion = &suggestions[0];
assert_eq!(suggestion.confidence, ConfidenceTier::High);
assert_eq!(suggestion.risk, RiskLevel::High);
assert_eq!(suggestion.reason, SuggestionReason::ManuallyBypassed);
assert!(suggestion.suggest_path_specific);
}
#[test]
fn scenario_mixed_risk_commands() {
let mut entries = Vec::new();
for _ in 0..10 {
entries.push(CommandEntryInfo {
command: "npm run lint".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
});
}
for _ in 0..10 {
entries.push(CommandEntryInfo {
command: "rm -rf /tmp/build".to_string(),
working_dir: "/data/projects/app".to_string(),
was_bypassed: false,
});
}
let all_suggestions = generate_enhanced_suggestions(&entries, 3);
let safe_suggestions = filter_by_risk(all_suggestions.clone(), RiskLevel::Low);
assert_eq!(safe_suggestions.len(), 1);
assert!(safe_suggestions[0].cluster.commands[0].contains("npm"));
let confident_suggestions = filter_by_confidence(all_suggestions, ConfidenceTier::High);
assert_eq!(confident_suggestions.len(), 2);
}