Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions src/pipeline/repo_risk_assessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ impl RepoRiskAssessor {
message: "No Go file risk scores are available.",
});
}
if matches!(confidence, "low" | "none") {
if scored_file_count > 0 && matches!(confidence, "low" | "none") {
limitations.push(ProjectRiskLimitation {
code: "low_language_coverage",
message: "Only a small share of active files has Go risk scores.",
});
}
if go_score_coverage.is_some_and(|coverage| coverage < 1.0) {
if scored_file_count > 0 && go_score_coverage.is_some_and(|coverage| coverage < 1.0) {
limitations.push(ProjectRiskLimitation {
code: "partial_go_score_coverage",
message: "Some active Go files do not have persisted risk scores.",
Expand Down Expand Up @@ -387,6 +387,10 @@ mod tests {
.limitations
.iter()
.any(|limitation| limitation.code == "no_scored_files"));
assert!(!assessment
.limitations
.iter()
.any(|limitation| limitation.code == "low_language_coverage"));
}

#[test]
Expand Down Expand Up @@ -433,6 +437,61 @@ mod tests {
.any(|limitation| limitation.code == "low_language_coverage"));
}

#[test]
fn limitations_are_direct_for_zero_scored_files() {
let assessment = RepoRiskAssessor::new().assess(&RepoRiskInput {
active_file_count: 3,
active_go_file_count: 2,
git_index_status: "available".to_owned(),
files: Vec::new(),
terms: Vec::new(),
});

assert_eq!(limitation_codes(&assessment), vec!["no_scored_files"]);
assert_eq!(assessment.confidence, "none");
}

#[test]
fn limitations_warn_for_partial_scored_files() {
let assessment = RepoRiskAssessor::new().assess(&RepoRiskInput {
active_file_count: 10,
active_go_file_count: 2,
git_index_status: "available".to_owned(),
files: vec![file("a.go", 0.3)],
terms: Vec::new(),
});

assert_eq!(
limitation_codes(&assessment),
vec!["low_language_coverage", "partial_go_score_coverage"]
);
assert_eq!(assessment.confidence, "low");
assert_eq!(assessment.go_score_coverage, Some(0.5));
}

#[test]
fn limitations_are_empty_for_healthy_go_scoring_coverage() {
let assessment = RepoRiskAssessor::new().assess(&RepoRiskInput {
active_file_count: 3,
active_go_file_count: 3,
git_index_status: "available".to_owned(),
files: vec![file("a.go", 0.3), file("b.go", 0.4), file("c.go", 0.5)],
terms: Vec::new(),
});

assert_eq!(limitation_codes(&assessment), Vec::<&str>::new());
assert_eq!(assessment.confidence, "high");
assert_eq!(assessment.go_score_coverage, Some(1.0));
}

fn limitation_codes(assessment: &super::RepoRiskAssessment) -> Vec<&'static str> {
assessment
.limitations
.iter()
.map(|limitation| limitation.code)
.collect()
}

fn file(relative_path: &str, score: f64) -> ProjectFileRiskInput {
ProjectFileRiskInput {
relative_path: relative_path.to_owned(),
Expand Down
42 changes: 42 additions & 0 deletions tests/scanner_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,40 @@ fn scan_summary_reports_no_go_coverage_and_limitation() {
assert!(!stdout.contains("files_analyzed"));
assert!(stdout.contains("Top Hotspots\n none"));
assert!(stdout.contains(" - No Go file risk scores are available"));
assert!(!stdout.contains(" - Only a small share of active files has Go risk scores"));
assert!(!stdout.contains(" - \n"));
assert!(!stdout.contains(" - No Go file risk scores are available."));
assert!(!stdout.contains("Index\n .hotpath/index.sqlite"));
}

#[test]
fn scan_json_reports_direct_no_score_limitation_without_overlap() {
let fixture = Fixture::new("scan-json-no-score-limitations");
fixture.write("README.md", "hello\n");

let output = hotpath(&["scan", "--json"], &fixture.path);

assert!(
output.status.success(),
"hotpath scan --json failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());

let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
let json: Value = serde_json::from_str(&stdout).expect("stdout should be JSON");

assert_eq!(json["risk"]["band"], "unavailable");
let codes = limitation_codes(&json);
assert_eq!(codes.first(), Some(&"no_scored_files"));
assert!(codes.contains(&"git_index_unavailable"));
assert!(!codes.contains(&"low_language_coverage"));
assert!(!codes.contains(&"partial_go_score_coverage"));
assert_json_has_no_empty_or_placeholder_limitations(&json);
assert_json_limitation_messages_are_sentences(&json);
}

#[test]
fn hotspots_prints_ranked_go_file_table_from_completed_index() {
let fixture = Fixture::new("hotspots-table");
Expand Down Expand Up @@ -1762,6 +1791,19 @@ fn assert_json_has_no_empty_or_placeholder_limitations(json: &Value) {
}
}

fn limitation_codes(json: &Value) -> Vec<&str> {
json["limitations"]
.as_array()
.expect("limitations should be an array")
.iter()
.map(|limitation| {
limitation["code"]
.as_str()
.expect("limitation code should be a string")
})
.collect()
}

fn create_index_lock(root: &Path, contents: &str) {
let index_dir = root.join(".hotpath");
fs::create_dir_all(&index_dir).expect("index dir should be created");
Expand Down
Loading