#[cfg(test)]
mod tests {
use crate::views::scan::explain::explain_check;
use crate::views::scan::progress::owl_position;
use crate::views::scan::*;
#[test]
fn test_findings_filter_all() {
use crate::types::Severity;
let filter = FindingsFilter::All;
assert!(filter.matches(Severity::Critical));
assert!(filter.matches(Severity::High));
assert!(filter.matches(Severity::Medium));
assert!(filter.matches(Severity::Low));
assert!(filter.matches(Severity::Info));
}
#[test]
fn test_findings_filter_critical() {
use crate::types::Severity;
let filter = FindingsFilter::Critical;
assert!(filter.matches(Severity::Critical));
assert!(!filter.matches(Severity::High));
assert!(!filter.matches(Severity::Medium));
assert!(!filter.matches(Severity::Low));
assert!(!filter.matches(Severity::Info));
}
#[test]
fn test_findings_filter_from_key() {
assert_eq!(
FindingsFilter::from_key('c'),
Some(FindingsFilter::Critical)
);
assert_eq!(FindingsFilter::from_key('a'), Some(FindingsFilter::All));
assert_eq!(FindingsFilter::from_key('h'), Some(FindingsFilter::High));
assert_eq!(FindingsFilter::from_key('m'), Some(FindingsFilter::Medium));
assert_eq!(FindingsFilter::from_key('l'), Some(FindingsFilter::Low));
assert_eq!(FindingsFilter::from_key('x'), None);
}
#[test]
fn test_layer_progress_default() {
let state = ScanViewState::default();
assert_eq!(state.layer_progress.len(), 5);
for layer in &state.layer_progress {
assert_eq!(layer.status, LayerStatus::Waiting);
}
assert!(!state.scanning);
assert!(!state.detail_open);
}
#[test]
fn t902_puzzle_header_all_locked() {
let state = ScanViewState::default();
let pos = owl_position(&state.layer_progress);
assert_eq!(pos, 0);
for layer in &state.layer_progress {
assert_eq!(layer.status, LayerStatus::Waiting);
}
}
#[test]
fn t902_puzzle_header_partial() {
let mut state = ScanViewState::default();
state.layer_progress[0].status = LayerStatus::Complete;
state.layer_progress[1].status = LayerStatus::Complete;
state.layer_progress[2].status = LayerStatus::Running;
state.layer_progress[2].current = 3;
state.layer_progress[2].total = 5;
let pos = owl_position(&state.layer_progress);
assert_eq!(pos, 2); }
#[test]
fn t902_owl_position_running() {
let mut state = ScanViewState::default();
state.layer_progress[3].status = LayerStatus::Running;
let pos = owl_position(&state.layer_progress);
assert_eq!(pos, 3); }
#[test]
fn test_explain_check_strips_layer_prefix() {
let (desc, _, _) = explain_check("declaration-conformity");
assert!(
desc.contains("Declaration of Conformity"),
"bare key should match"
);
let (desc2, _, _) = explain_check("l2-declaration-conformity");
assert!(
desc2.contains("Declaration of Conformity"),
"l2- prefix should be stripped"
);
let (desc3, _, _) = explain_check("l4-bare-openai");
assert!(
desc3.contains("Bare API Call"),
"l4- prefix should be stripped"
);
let (desc4, _, _) = explain_check("cross-doc-code-mismatch");
assert!(
desc4.contains("Documentation claims"),
"cross- prefix should be stripped"
);
let (desc5, _, _) = explain_check("l2-unknown-check-xyz");
assert!(
desc5.contains("Compliance check"),
"unknown should get default"
);
}
fn render_scan_to_string(app: &crate::app::App, width: u16, height: u16) -> String {
let backend = ratatui::backend::TestBackend::new(width, height);
let mut terminal = ratatui::Terminal::new(backend).expect("terminal");
terminal
.draw(|frame| render_scan_view(frame, frame.area(), app))
.expect("render");
let buf = terminal.backend().buffer().clone();
let mut output = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
output.push_str(buf[(x, y)].symbol());
}
output.push('\n');
}
output
}
#[test]
fn snapshot_scan_no_results() {
crate::theme::init_theme("dark");
let app = crate::app::App::new(crate::config::TuiConfig::default());
let buf = render_scan_to_string(&app, 80, 24);
insta::assert_snapshot!(buf);
}
#[test]
fn test_scan_view_no_results() {
crate::theme::init_theme("dark");
let backend = ratatui::backend::TestBackend::new(80, 24);
let mut terminal = ratatui::Terminal::new(backend).expect("terminal");
let app = crate::app::App::new(crate::config::TuiConfig::default());
terminal
.draw(|frame| render_scan_view(frame, frame.area(), &app))
.expect("render");
}
fn make_scan_findings() -> Vec<crate::types::Finding> {
use crate::types::{CodeContext, CodeContextLine, Finding, FixDiff, Severity};
vec![
Finding {
check_id: "l4-bare-anthropic".to_string(),
r#type: crate::types::CheckResultType::Fail,
message: "Bare Anthropic API call without compliance wrapper".to_string(),
severity: Severity::Critical,
obligation_id: Some("OBL-015".to_string()),
article_reference: Some("Art. 50(1)".to_string()),
fix: Some(
"- const c = new Anthropic();\n+ const c = complior(new Anthropic());"
.to_string(),
),
file: Some("src/chat/anthropic.ts".to_string()),
line: Some(8),
code_context: Some(CodeContext {
lines: vec![
CodeContextLine {
num: 6,
content: "import Anthropic from 'anthropic';".to_string(),
},
CodeContextLine {
num: 7,
content: "".to_string(),
},
CodeContextLine {
num: 8,
content: "const c = new Anthropic();".to_string(),
},
CodeContextLine {
num: 9,
content: "const resp = await c.messages.create({".to_string(),
},
CodeContextLine {
num: 10,
content: " model: 'claude-3',".to_string(),
},
],
start_line: 6,
highlight_line: Some(8),
}),
fix_diff: Some(FixDiff {
before: vec!["const c = new Anthropic();".to_string()],
after: vec!["const c = complior(new Anthropic());".to_string()],
start_line: 8,
file_path: "src/chat/anthropic.ts".to_string(),
import_line: None,
}),
priority: None,
confidence: None,
confidence_level: None,
evidence: None,
explanation: None,
agent_id: None,
doc_quality: None,
l5_analyzed: None,
},
Finding {
check_id: "l2-fria".to_string(),
r#type: crate::types::CheckResultType::Fail,
message: "Missing FRIA document".to_string(),
severity: Severity::High,
obligation_id: Some("OBL-006".to_string()),
article_reference: Some("Art. 27(1)".to_string()),
fix: Some(
"# Fundamental Rights Impact Assessment\n\n## 1. Purpose\n...".to_string(),
),
file: None,
line: None,
code_context: None,
fix_diff: None,
priority: None,
confidence: None,
confidence_level: None,
evidence: None,
explanation: None,
agent_id: None,
doc_quality: None,
l5_analyzed: None,
},
Finding {
check_id: "l3-compliance-metadata".to_string(),
r#type: crate::types::CheckResultType::Fail,
message: "Missing compliance metadata in package.json".to_string(),
severity: Severity::Medium,
obligation_id: Some("OBL-012".to_string()),
article_reference: Some("Art. 53(1)".to_string()),
fix: Some("+ \"compliance\": {\n+ \"framework\": \"eu-ai-act\"\n+ }".to_string()),
file: Some("package.json".to_string()),
line: None,
code_context: None,
fix_diff: None,
priority: None,
confidence: None,
confidence_level: None,
evidence: None,
explanation: None,
agent_id: None,
doc_quality: None,
l5_analyzed: None,
},
Finding {
check_id: "l2-monitoring-policy".to_string(),
r#type: crate::types::CheckResultType::Fail,
message: "Missing post-market monitoring policy".to_string(),
severity: Severity::High,
obligation_id: Some("OBL-009".to_string()),
article_reference: Some("Art. 72(1)".to_string()),
fix: None,
file: None,
line: None,
code_context: None,
fix_diff: None,
priority: None,
confidence: None,
confidence_level: None,
evidence: None,
explanation: None,
agent_id: None,
doc_quality: None,
l5_analyzed: None,
},
]
}
fn make_scan_result() -> crate::types::ScanResult {
crate::types::ScanResult {
score: crate::types::ScoreBreakdown {
total_score: 32.0,
zone: crate::types::Zone::Red,
category_scores: vec![],
critical_cap_applied: false,
total_checks: 20,
passed_checks: 8,
failed_checks: 12,
skipped_checks: 0,
confidence_summary: None,
},
findings: make_scan_findings(),
project_path: "cli/".to_string(),
scanned_at: "2026-02-28T12:00:00Z".to_string(),
duration: 450,
files_scanned: 24,
files_excluded: None,
deep_analysis: None,
l5_cost: None,
regulation_version: None,
tier: None,
external_tool_results: None,
agent_summaries: None,
filter_context: None,
top_actions: None,
}
}
#[test]
fn snapshot_scan_split_layout() {
crate::theme::init_theme("dark");
let mut app = crate::app::App::new(crate::config::TuiConfig::default());
app.last_scan = Some(make_scan_result());
app.scan_view.set_complete(24);
app.scan_view.selected_finding = Some(0);
let buf = render_scan_to_string(&app, 100, 30);
insta::assert_snapshot!(buf);
}
#[test]
fn snapshot_scan_detail_open() {
crate::theme::init_theme("dark");
let mut app = crate::app::App::new(crate::config::TuiConfig::default());
app.last_scan = Some(make_scan_result());
app.scan_view.set_complete(24);
app.scan_view.selected_finding = Some(0);
app.scan_view.detail_open = true;
let buf = render_scan_to_string(&app, 100, 30);
insta::assert_snapshot!(buf);
}
#[test]
fn test_finding_type_classification() {
let findings = make_scan_findings();
assert_eq!(findings[0].finding_type(), crate::types::FindingType::A); assert_eq!(findings[1].finding_type(), crate::types::FindingType::B); assert_eq!(findings[2].finding_type(), crate::types::FindingType::C); assert_eq!(findings[3].finding_type(), crate::types::FindingType::B); }
#[test]
fn test_finding_predicted_impact() {
let findings = make_scan_findings();
assert_eq!(findings[0].predicted_impact(), 8); assert_eq!(findings[1].predicted_impact(), 5); assert_eq!(findings[2].predicted_impact(), 3); assert_eq!(findings[3].predicted_impact(), 5); }
#[test]
fn test_finding_file_line_label() {
let findings = make_scan_findings();
assert_eq!(
findings[0].file_line_label(),
Some("src/chat/anthropic.ts:8".to_string())
);
assert_eq!(findings[1].file_line_label(), None); assert_eq!(
findings[2].file_line_label(),
Some("package.json".to_string())
); }
#[test]
fn test_scan_split_resize() {
let mut state = ScanViewState::default();
assert_eq!(state.scan_split_pct, 45);
state.scan_split_pct = state.scan_split_pct.saturating_sub(5).max(25);
assert_eq!(state.scan_split_pct, 40);
state.scan_split_pct = (state.scan_split_pct + 5).min(75);
assert_eq!(state.scan_split_pct, 45);
state.scan_split_pct = 25;
state.scan_split_pct = state.scan_split_pct.saturating_sub(5).max(25);
assert_eq!(state.scan_split_pct, 25);
}
#[test]
fn test_progress_collapsed_on_complete() {
let mut state = ScanViewState::default();
assert!(!state.progress_collapsed);
state.set_complete(10);
assert!(state.progress_collapsed);
}
#[test]
fn scan_findings_grouped_by_agent() {
crate::theme::init_theme("dark");
let mut app = crate::app::App::new(crate::config::TuiConfig::default());
app.last_scan = Some(make_scan_result());
app.scan_view.set_complete(24);
app.scan_view.selected_finding = Some(0);
app.passport_view.loaded_passports = vec![
serde_json::json!({
"name": "chat-agent",
"autonomy_level": "L3",
"compliance": { "complior_score": 75 },
"source_files": ["src/chat/anthropic.ts", "src/chat/openai.ts"]
}),
serde_json::json!({
"name": "config-agent",
"autonomy_level": "L2",
"compliance": { "complior_score": 60 },
"source_files": ["package.json"]
}),
];
let buf = render_scan_to_string(&app, 120, 40);
assert!(
buf.contains("chat-agent"),
"Should show chat-agent group header"
);
assert!(
buf.contains("config-agent"),
"Should show config-agent group header"
);
assert!(
buf.contains("Project"),
"Findings without agent match should be in Project group"
);
}
#[test]
fn scan_findings_no_grouping_without_passports() {
crate::theme::init_theme("dark");
let mut app = crate::app::App::new(crate::config::TuiConfig::default());
app.last_scan = Some(make_scan_result());
app.scan_view.set_complete(24);
app.scan_view.selected_finding = Some(0);
let buf = render_scan_to_string(&app, 120, 40);
assert!(buf.contains("CRITICAL"), "Should show severity headers");
assert!(buf.contains("HIGH"), "Should show severity headers");
assert!(
!buf.contains("Project"),
"Should not show agent group when no passports"
);
}
}