#[cfg(not(feature = "rich-output"))]
use super::terminal_width;
#[cfg(not(feature = "rich-output"))]
use super::theme::BorderStyle;
use super::theme::Theme;
use crate::evaluator::{EvaluationDecision, EvaluationResult, PatternMatch};
#[cfg(feature = "rich-output")]
use crate::output::rich_theme::RichThemeExt;
use crate::packs::Severity;
#[cfg(not(feature = "rich-output"))]
use ratatui::style::Color;
#[cfg(feature = "rich-output")]
#[allow(unused_imports)]
use rich_rust::prelude::*;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct TestResultBox {
pub command: String,
pub result: TestOutcome,
}
#[derive(Debug, Clone)]
pub enum TestOutcome {
Blocked {
pattern_id: Option<String>,
pack_id: Option<String>,
severity: Option<Severity>,
reason: String,
explanation: Option<String>,
confidence: Option<f64>,
},
Allowed {
reason: AllowedReason,
},
}
#[derive(Debug, Clone)]
pub enum AllowedReason {
NoPatternMatch,
AllowlistMatch {
entry: String,
layer: String,
},
BudgetExhausted,
}
impl TestResultBox {
#[must_use]
pub fn from_evaluation(command: impl Into<String>, eval: &EvaluationResult) -> Self {
let command = command.into();
let result = match eval.decision {
EvaluationDecision::Deny => {
let pattern_info = eval.pattern_info.as_ref();
TestOutcome::Blocked {
pattern_id: pattern_info.and_then(|p| p.pattern_name.clone()),
pack_id: pattern_info.and_then(|p| p.pack_id.clone()),
severity: pattern_info.and_then(|p| p.severity),
reason: pattern_info
.map(|p| p.reason.clone())
.unwrap_or_else(|| "Pattern matched".to_string()),
explanation: pattern_info.and_then(|p| p.explanation.clone()),
confidence: pattern_info.and_then(confidence_from_severity),
}
}
EvaluationDecision::Allow => {
if eval.skipped_due_to_budget {
TestOutcome::Allowed {
reason: AllowedReason::BudgetExhausted,
}
} else if let Some(override_info) = &eval.allowlist_override {
TestOutcome::Allowed {
reason: AllowedReason::AllowlistMatch {
entry: override_info.reason.clone(),
layer: format!("{:?}", override_info.layer),
},
}
} else {
TestOutcome::Allowed {
reason: AllowedReason::NoPatternMatch,
}
}
}
};
Self { command, result }
}
#[must_use]
pub fn blocked(
command: impl Into<String>,
pattern_id: Option<String>,
pack_id: Option<String>,
severity: Option<Severity>,
reason: impl Into<String>,
confidence: Option<f64>,
) -> Self {
Self {
command: command.into(),
result: TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason: reason.into(),
explanation: None,
confidence,
},
}
}
#[must_use]
pub fn allowed_no_match(command: impl Into<String>) -> Self {
Self {
command: command.into(),
result: TestOutcome::Allowed {
reason: AllowedReason::NoPatternMatch,
},
}
}
#[must_use]
pub fn allowed_by_allowlist(
command: impl Into<String>,
entry: impl Into<String>,
layer: impl Into<String>,
) -> Self {
Self {
command: command.into(),
result: TestOutcome::Allowed {
reason: AllowedReason::AllowlistMatch {
entry: entry.into(),
layer: layer.into(),
},
},
}
}
#[must_use]
pub const fn is_blocked(&self) -> bool {
matches!(self.result, TestOutcome::Blocked { .. })
}
#[must_use]
pub fn render(&self, theme: &Theme) -> String {
#[cfg(feature = "rich-output")]
{
self.render_rich(theme)
}
#[cfg(not(feature = "rich-output"))]
match theme.border_style {
BorderStyle::Unicode => {
let output = self.render_unicode(theme);
if theme.colors_enabled {
output
} else {
strip_ansi_codes(&output)
}
}
BorderStyle::Ascii => self.render_ascii(theme),
BorderStyle::None => {
let output = self.render_minimal(theme);
if theme.colors_enabled {
output
} else {
strip_ansi_codes(&output)
}
}
}
}
#[cfg(feature = "rich-output")]
fn render_rich(&self, theme: &Theme) -> String {
use rich_rust::r#box::{DOUBLE, HEAVY, ROUNDED};
use rich_rust::prelude::*;
let (title, border_style, header_color): (
&str,
&'static rich_rust::r#box::BoxChars,
String,
) = match &self.result {
TestOutcome::Blocked { severity, .. } => {
let box_style = match severity {
Some(Severity::Critical) => &DOUBLE,
Some(Severity::High) => &HEAVY,
_ => &ROUNDED,
};
let color_str = theme.error_markup();
(" WOULD BE BLOCKED ", box_style, color_str)
}
TestOutcome::Allowed { .. } => (" WOULD BE ALLOWED ", &ROUNDED, theme.success_markup()),
};
let mut lines = Vec::new();
lines.push(format!(
"[dim]Command:[/] [bold]{cmd}[/]",
cmd = self.command
));
match &self.result {
TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason,
confidence,
explanation,
} => {
if let Some(pattern) = pattern_id {
lines.push(format!("[dim]Pattern:[/] [magenta]{pattern}[/]"));
}
if let Some(pack) = pack_id {
let sev = severity
.map(|s| format!(" ({})", severity_label(s)))
.unwrap_or_default();
lines.push(format!("[dim]Pack:[/] [cyan]{pack}[/][dim]{sev}[/]"));
}
if let Some(conf) = confidence {
let bar = render_confidence_bar(*conf);
lines.push(format!(
"[dim]Confidence:[/] {bar} {conf:.0}%",
conf = conf * 100.0
));
}
lines.push(format!("[dim]Reason:[/] {reason}"));
if let Some(text) = explanation {
lines.push(format!("[dim]Explanation:[/] {text}"));
}
}
TestOutcome::Allowed { reason } => {
let reason_text = match reason {
AllowedReason::NoPatternMatch => "No pattern matches".to_string(),
AllowedReason::AllowlistMatch { entry, layer } => {
format!("Allowlist: [italic]\"{entry}\"[/] ({layer})")
}
AllowedReason::BudgetExhausted => {
"[yellow]Budget exhausted (fail-open)[/]".to_string()
}
};
lines.push(format!("[dim]Reason:[/] {reason_text}"));
}
}
let content_str = lines.join("\n");
let border_color_str = if header_color.contains("red") {
"red"
} else if header_color.contains("green") {
"green"
} else if header_color.contains("yellow") {
"yellow"
} else {
"white"
};
let width = super::terminal_width() as usize;
Panel::from_text(&content_str)
.title(format!("[{header_color}]{title}[/]"))
.box_style(border_style)
.border_style(Style::parse(border_color_str).unwrap_or_default())
.padding((1, 2))
.render_plain(width)
}
#[must_use]
pub fn render_plain(&self) -> String {
let mut output = String::new();
match &self.result {
TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason,
confidence,
explanation,
} => {
let _ = writeln!(output, "WOULD BE BLOCKED");
let _ = writeln!(output);
let _ = writeln!(output, " Command: {}", self.command);
if let Some(pattern) = pattern_id {
let _ = writeln!(output, " Pattern: {pattern}");
}
if let Some(pack) = pack_id {
let severity_str = severity
.map(|s| format!(" (severity: {})", severity_label(s)))
.unwrap_or_default();
let _ = writeln!(output, " Pack: {pack}{severity_str}");
}
if let Some(conf) = confidence {
let _ = writeln!(output, " Confidence: {conf:.2}");
}
let _ = writeln!(output, " Reason: {reason}");
if let Some(text) = explanation {
let _ = writeln!(output, " Explanation: {text}");
}
}
TestOutcome::Allowed { reason } => {
let _ = writeln!(output, "WOULD BE ALLOWED");
let _ = writeln!(output);
let _ = writeln!(output, " Command: {}", self.command);
match reason {
AllowedReason::NoPatternMatch => {
let _ = writeln!(output, " Reason: No pattern matches");
}
AllowedReason::AllowlistMatch { entry, layer } => {
let _ = writeln!(output, " Reason: Allowlist match: \"{entry}\"");
let _ = writeln!(output, " Layer: {layer}");
}
AllowedReason::BudgetExhausted => {
let _ = writeln!(
output,
" Reason: Evaluation budget exhausted (fail-open)"
);
}
}
}
}
output
}
#[cfg(not(feature = "rich-output"))]
#[allow(clippy::too_many_lines)]
fn render_unicode(&self, theme: &Theme) -> String {
let width = terminal_width().saturating_sub(4).max(40) as usize;
let mut output = String::new();
let (header, header_color) = match &self.result {
TestOutcome::Blocked { .. } => (" WOULD BE BLOCKED ", theme.error_color),
TestOutcome::Allowed { .. } => (" WOULD BE ALLOWED ", theme.success_color),
};
let color_code = ansi_color_code(header_color);
let header_len = header.chars().count();
let top_pad = width.saturating_sub(header_len);
let _ = writeln!(
output,
"\x1b[{}m\u{256d}{}\u{256e}\x1b[0m",
&color_code,
"\u{2500}".repeat(width)
);
let _ = writeln!(
output,
"\x1b[{}m\u{2502}\x1b[0m\x1b[1;{}m{}\x1b[0m{}\x1b[{}m\u{2502}\x1b[0m",
&color_code,
&color_code,
header,
" ".repeat(top_pad),
&color_code
);
let _ = writeln!(
output,
"\x1b[{}m\u{251c}{}\u{2524}\x1b[0m",
&color_code,
"\u{2500}".repeat(width)
);
match &self.result {
TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason,
confidence,
explanation,
} => {
self.render_unicode_row(&mut output, "Command:", &self.command, width, &color_code);
if let Some(pattern) = pattern_id {
self.render_unicode_row(&mut output, "Pattern:", pattern, width, &color_code);
}
if let Some(pack) = pack_id {
let severity_str = severity
.map(|s| format!(" (severity: {})", severity_label(s)))
.unwrap_or_default();
self.render_unicode_row(
&mut output,
"Pack:",
&format!("{pack}{severity_str}"),
width,
&color_code,
);
}
if let Some(conf) = confidence {
self.render_unicode_row(
&mut output,
"Confidence:",
&format!("{conf:.2}"),
width,
&color_code,
);
}
self.render_unicode_row(&mut output, "Reason:", reason, width, &color_code);
if let Some(text) = explanation {
self.render_unicode_row(&mut output, "Explanation:", text, width, &color_code);
}
}
TestOutcome::Allowed { reason } => {
self.render_unicode_row(&mut output, "Command:", &self.command, width, &color_code);
match reason {
AllowedReason::NoPatternMatch => {
self.render_unicode_row(
&mut output,
"Reason:",
"No pattern matches",
width,
&color_code,
);
}
AllowedReason::AllowlistMatch { entry, layer } => {
self.render_unicode_row(
&mut output,
"Reason:",
&format!("Allowlist match: \"{entry}\""),
width,
&color_code,
);
self.render_unicode_row(&mut output, "Layer:", layer, width, &color_code);
}
AllowedReason::BudgetExhausted => {
self.render_unicode_row(
&mut output,
"Reason:",
"Evaluation budget exhausted (fail-open)",
width,
&color_code,
);
}
}
}
}
let _ = writeln!(
output,
"\x1b[{}m\u{2570}{}\u{256f}\x1b[0m",
&color_code,
"\u{2500}".repeat(width)
);
output
}
#[cfg(not(feature = "rich-output"))]
fn render_unicode_row(
&self,
output: &mut String,
label: &str,
value: &str,
width: usize,
color_code: &str,
) {
let label_width = 12; let content = format!("{label:<label_width$}{value}");
let content_len = content.chars().count();
let padding = width.saturating_sub(content_len + 4);
let _ = writeln!(
output,
"\x1b[{color_code}m\u{2502}\x1b[0m {content}{}\x1b[{color_code}m\u{2502}\x1b[0m",
" ".repeat(padding),
);
}
#[cfg(not(feature = "rich-output"))]
fn render_ascii(&self, _theme: &Theme) -> String {
let width = terminal_width().saturating_sub(4).max(40) as usize;
let mut output = String::new();
let header = match &self.result {
TestOutcome::Blocked { .. } => " WOULD BE BLOCKED ",
TestOutcome::Allowed { .. } => " WOULD BE ALLOWED ",
};
let header_len = header.chars().count();
let top_pad = width.saturating_sub(header_len);
let _ = writeln!(output, "+{}+", "-".repeat(width));
let _ = writeln!(output, "|{}{}|", header, " ".repeat(top_pad));
let _ = writeln!(output, "+{}+", "-".repeat(width));
match &self.result {
TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason,
confidence,
explanation,
} => {
self.render_ascii_row(&mut output, "Command:", &self.command, width);
if let Some(pattern) = pattern_id {
self.render_ascii_row(&mut output, "Pattern:", pattern, width);
}
if let Some(pack) = pack_id {
let severity_str = severity
.map(|s| format!(" (severity: {})", severity_label(s)))
.unwrap_or_default();
self.render_ascii_row(
&mut output,
"Pack:",
&format!("{pack}{severity_str}"),
width,
);
}
if let Some(conf) = confidence {
self.render_ascii_row(&mut output, "Confidence:", &format!("{conf:.2}"), width);
}
self.render_ascii_row(&mut output, "Reason:", reason, width);
if let Some(text) = explanation {
self.render_ascii_row(&mut output, "Explanation:", text, width);
}
}
TestOutcome::Allowed { reason } => {
self.render_ascii_row(&mut output, "Command:", &self.command, width);
match reason {
AllowedReason::NoPatternMatch => {
self.render_ascii_row(&mut output, "Reason:", "No pattern matches", width);
}
AllowedReason::AllowlistMatch { entry, layer } => {
self.render_ascii_row(
&mut output,
"Reason:",
&format!("Allowlist match: \"{entry}\""),
width,
);
self.render_ascii_row(&mut output, "Layer:", layer, width);
}
AllowedReason::BudgetExhausted => {
self.render_ascii_row(
&mut output,
"Reason:",
"Evaluation budget exhausted (fail-open)",
width,
);
}
}
}
}
let _ = writeln!(output, "+{}+", "-".repeat(width));
output
}
#[cfg(not(feature = "rich-output"))]
fn render_ascii_row(&self, output: &mut String, label: &str, value: &str, width: usize) {
let label_width = 12; let content = format!("{label:<label_width$}{value}");
let content_len = content.chars().count();
let padding = width.saturating_sub(content_len + 4);
let _ = writeln!(output, "| {content}{}|", " ".repeat(padding));
}
#[cfg(not(feature = "rich-output"))]
fn render_minimal(&self, theme: &Theme) -> String {
let mut output = String::new();
let (header, header_color) = match &self.result {
TestOutcome::Blocked { .. } => ("WOULD BE BLOCKED", theme.error_color),
TestOutcome::Allowed { .. } => ("WOULD BE ALLOWED", theme.success_color),
};
let color_code = ansi_color_code(header_color);
let _ = writeln!(output, "\x1b[1;{color_code}m{header}\x1b[0m");
let _ = writeln!(output);
match &self.result {
TestOutcome::Blocked {
pattern_id,
pack_id,
severity,
reason,
confidence,
explanation,
} => {
let _ = writeln!(output, " Command: {}", self.command);
if let Some(pattern) = pattern_id {
let _ = writeln!(output, " Pattern: {pattern}");
}
if let Some(pack) = pack_id {
let severity_str = severity
.map(|s| format!(" (severity: {})", severity_label(s)))
.unwrap_or_default();
let _ = writeln!(output, " Pack: {pack}{severity_str}");
}
if let Some(conf) = confidence {
let _ = writeln!(output, " Confidence: {conf:.2}");
}
let _ = writeln!(output, " Reason: {reason}");
if let Some(text) = explanation {
let _ = writeln!(output, " Explanation: {text}");
}
}
TestOutcome::Allowed { reason } => {
let _ = writeln!(output, " Command: {}", self.command);
match reason {
AllowedReason::NoPatternMatch => {
let _ = writeln!(output, " Reason: No pattern matches");
}
AllowedReason::AllowlistMatch { entry, layer } => {
let _ = writeln!(output, " Reason: Allowlist match: \"{entry}\"");
let _ = writeln!(output, " Layer: {layer}");
}
AllowedReason::BudgetExhausted => {
let _ = writeln!(
output,
" Reason: Evaluation budget exhausted (fail-open)"
);
}
}
}
}
output
}
}
#[cfg(not(feature = "rich-output"))]
fn ansi_color_code(color: Color) -> String {
match color {
Color::Reset => "0".to_string(),
Color::Black => "30".to_string(),
Color::Red => "31".to_string(),
Color::Green => "32".to_string(),
Color::Yellow => "33".to_string(),
Color::Blue => "34".to_string(),
Color::Magenta => "35".to_string(),
Color::Cyan => "36".to_string(),
Color::Gray => "37".to_string(),
Color::DarkGray => "90".to_string(),
Color::LightRed => "91".to_string(),
Color::LightGreen => "92".to_string(),
Color::LightYellow => "93".to_string(),
Color::LightBlue => "94".to_string(),
Color::LightMagenta => "95".to_string(),
Color::LightCyan => "96".to_string(),
Color::White => "97".to_string(),
Color::Rgb(r, g, b) => format!("38;2;{r};{g};{b}"),
Color::Indexed(index) => format!("38;5;{index}"),
}
}
fn severity_label(severity: Severity) -> &'static str {
match severity {
Severity::Critical => "critical",
Severity::High => "high",
Severity::Medium => "medium",
Severity::Low => "low",
}
}
fn confidence_from_severity(pattern: &PatternMatch) -> Option<f64> {
pattern.severity.map(|s| match s {
Severity::Critical => 0.95,
Severity::High => 0.85,
Severity::Medium => 0.70,
Severity::Low => 0.50,
})
}
#[cfg(not(feature = "rich-output"))]
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut in_escape = false;
for c in s.chars() {
if c == '\x1b' {
in_escape = true;
continue;
}
if in_escape {
if c == 'm' {
in_escape = false;
}
continue;
}
result.push(c);
}
result
}
#[cfg(feature = "rich-output")]
fn render_confidence_bar(confidence: f64) -> String {
let filled = (confidence * 10.0).round() as usize;
let empty = 10usize.saturating_sub(filled);
let color = if confidence >= 0.8 {
"red"
} else if confidence >= 0.5 {
"yellow"
} else {
"green"
};
format!(
"[{color}]{}[/][dim]{}[/]",
"█".repeat(filled),
"░".repeat(empty)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blocked_result_plain_render() {
let result = TestResultBox::blocked(
"rm -rf /",
Some("filesystem.recursive_delete".to_string()),
Some("core".to_string()),
Some(Severity::Critical),
"Recursive deletion of root filesystem",
Some(0.95),
);
let output = result.render_plain();
assert!(output.contains("WOULD BE BLOCKED"));
assert!(output.contains("rm -rf /"));
assert!(output.contains("filesystem.recursive_delete"));
assert!(output.contains("core"));
assert!(output.contains("critical"));
assert!(output.contains("0.95"));
}
#[test]
fn test_allowed_no_match_plain_render() {
let result = TestResultBox::allowed_no_match("npm run build");
let output = result.render_plain();
assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("npm run build"));
assert!(output.contains("No pattern matches"));
}
#[test]
fn test_allowed_by_allowlist_plain_render() {
let result = TestResultBox::allowed_by_allowlist(
"git push --force",
"force push allowed",
"Project",
);
let output = result.render_plain();
assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("git push --force"));
assert!(output.contains("Allowlist match"));
assert!(output.contains("force push allowed"));
assert!(output.contains("Project"));
}
#[test]
fn test_is_blocked() {
let blocked = TestResultBox::blocked("rm -rf /", None, None, None, "dangerous", None);
assert!(blocked.is_blocked());
let allowed = TestResultBox::allowed_no_match("echo hello");
assert!(!allowed.is_blocked());
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_unicode_render_blocked() {
let theme = Theme::default();
let result = TestResultBox::blocked(
"git reset --hard",
Some("core.git.reset_hard".to_string()),
Some("core.git".to_string()),
Some(Severity::Critical),
"Destroys uncommitted changes",
Some(0.95),
);
let output = result.render(&theme);
assert!(output.contains('\u{256d}')); assert!(output.contains('\u{256f}')); assert!(output.contains("WOULD BE BLOCKED"));
assert!(output.contains("git reset --hard"));
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_unicode_render_allowed() {
let theme = Theme::default();
let result = TestResultBox::allowed_no_match("cargo build");
let output = result.render(&theme);
assert!(output.contains('\u{256d}')); assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("cargo build"));
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_ascii_render() {
let theme = Theme {
border_style: BorderStyle::Ascii,
colors_enabled: true,
..Default::default()
};
let result = TestResultBox::blocked(
"DROP TABLE users",
Some("database.drop_table".to_string()),
Some("database.postgresql".to_string()),
Some(Severity::High),
"Drops database table",
None,
);
let output = result.render(&theme);
assert!(output.contains('+'));
assert!(output.contains('-'));
assert!(output.contains("WOULD BE BLOCKED"));
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_no_color_render() {
let theme = Theme::no_color();
let result = TestResultBox::blocked(
"rm -rf ~",
Some("filesystem.rm_home".to_string()),
None,
Some(Severity::Critical),
"Deletes home directory",
None,
);
let output = result.render(&theme);
assert!(
!output.contains('\x1b'),
"No ANSI escapes should appear when colors are disabled"
);
assert!(output.contains("WOULD BE BLOCKED"));
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_minimal_render() {
let theme = Theme {
border_style: BorderStyle::None,
..Default::default()
};
let result = TestResultBox::allowed_no_match("ls -la");
let output = result.render(&theme);
assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("ls -la"));
assert!(!output.contains('\u{256d}'));
assert!(!output.contains('+'));
}
#[test]
fn test_from_evaluation_denied() {
let eval = EvaluationResult {
decision: EvaluationDecision::Deny,
pattern_info: Some(PatternMatch {
pack_id: Some("core.git".to_string()),
pattern_name: Some("reset_hard".to_string()),
severity: Some(Severity::Critical),
reason: "Destroys uncommitted changes".to_string(),
source: crate::evaluator::MatchSource::Pack,
matched_span: None,
matched_text_preview: None,
explanation: None,
suggestions: &[],
}),
allowlist_override: None,
effective_mode: Some(crate::packs::DecisionMode::Deny),
skipped_due_to_budget: false,
branch_context: None,
};
let result = TestResultBox::from_evaluation("git reset --hard HEAD", &eval);
assert!(result.is_blocked());
let output = result.render_plain();
assert!(output.contains("WOULD BE BLOCKED"));
assert!(output.contains("Destroys uncommitted changes"));
}
#[test]
fn test_from_evaluation_allowed() {
let eval = EvaluationResult::allowed();
let result = TestResultBox::from_evaluation("echo hello", &eval);
assert!(!result.is_blocked());
let output = result.render_plain();
assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("No pattern matches"));
}
#[test]
fn test_from_evaluation_budget_exhausted() {
let eval = EvaluationResult::allowed_due_to_budget();
let result = TestResultBox::from_evaluation("complex command", &eval);
assert!(!result.is_blocked());
let output = result.render_plain();
assert!(output.contains("WOULD BE ALLOWED"));
assert!(output.contains("budget exhausted"));
}
#[test]
#[cfg(not(feature = "rich-output"))]
fn test_strip_ansi_codes() {
let with_codes = "\x1b[31mRed text\x1b[0m and \x1b[32mgreen\x1b[0m";
let stripped = strip_ansi_codes(with_codes);
assert_eq!(stripped, "Red text and green");
}
#[test]
fn test_severity_labels() {
assert_eq!(severity_label(Severity::Critical), "critical");
assert_eq!(severity_label(Severity::High), "high");
assert_eq!(severity_label(Severity::Medium), "medium");
assert_eq!(severity_label(Severity::Low), "low");
}
#[test]
fn test_confidence_from_severity() {
let pattern = PatternMatch {
pack_id: None,
pattern_name: None,
severity: Some(Severity::Critical),
reason: String::new(),
source: crate::evaluator::MatchSource::Pack,
matched_span: None,
matched_text_preview: None,
explanation: None,
suggestions: &[],
};
assert_eq!(confidence_from_severity(&pattern), Some(0.95));
let pattern_high = PatternMatch {
severity: Some(Severity::High),
..pattern.clone()
};
assert_eq!(confidence_from_severity(&pattern_high), Some(0.85));
let pattern_none = PatternMatch {
severity: None,
..pattern
};
assert_eq!(confidence_from_severity(&pattern_none), None);
}
#[test]
fn test_unicode_command_preservation() {
let result = TestResultBox::blocked(
"rm -rf /path/with/émojis/🎉",
None,
None,
None,
"test",
None,
);
let output = result.render_plain();
assert!(output.contains("émojis"));
assert!(output.contains("🎉"));
}
}