use super::*;
use crate::bug_hunter::{Finding, FindingSeverity};
#[test]
fn test_parse_claim_header_standard() {
let result = parse_claim_header("### BH-01: Mutation-Based Invariant Falsification");
assert!(result.is_some());
let (id, title) = result.unwrap();
assert_eq!(id, "BH-01");
assert_eq!(title, "Mutation-Based Invariant Falsification");
}
#[test]
fn test_parse_claim_header_auth() {
let result = parse_claim_header("### AUTH-01: Token Validation");
assert!(result.is_some());
let (id, title) = result.unwrap();
assert_eq!(id, "AUTH-01");
assert_eq!(title, "Token Validation");
}
#[test]
fn test_parse_claim_header_no_id() {
let result = parse_claim_header("### Just a Title");
assert!(result.is_none());
}
#[test]
fn test_parse_claims_from_content() {
let content = r#"
## Section 1: Test
### BH-01: First Claim
Some text here.
### BH-02: Second Claim
More text.
"#;
let claims = parse_claims(content);
assert_eq!(claims.len(), 2);
assert_eq!(claims[0].id, "BH-01");
assert_eq!(claims[1].id, "BH-02");
}
#[test]
fn test_claim_status_display() {
assert_eq!(format!("{}", ClaimStatus::Verified), "✓ Verified");
assert_eq!(format!("{}", ClaimStatus::Warning), "⚠️ Warning");
assert_eq!(format!("{}", ClaimStatus::Failed), "✗ Failed");
assert_eq!(format!("{}", ClaimStatus::Pending), "○ Pending");
}
#[test]
fn test_remove_existing_status_blocks() {
let content = r#"### BH-01: Test Claim
<!-- bug-hunter-status -->
**Bug Hunter Status:** ✓ Verified
**Findings:** None ✓
<!-- /bug-hunter-status -->
Some content here.
"#;
let result = remove_existing_status_blocks(content);
assert!(!result.contains("bug-hunter-status"));
assert!(result.contains("### BH-01: Test Claim"));
assert!(result.contains("Some content here."));
}
#[test]
fn test_remove_existing_status_blocks_no_block() {
let content = "### BH-01: Test\n\nNo status block here.\n";
let result = remove_existing_status_blocks(content);
assert_eq!(result, "### BH-01: Test\n\nNo status block here.\n");
}
#[test]
fn test_find_claim_end() {
let content = "### BH-01: First Claim\n\nSome text.\n\n### BH-02: Second\n";
let pos = find_claim_end(content, "BH-01");
assert!(pos.is_some());
assert!(pos.unwrap() > 0);
}
#[test]
fn test_find_claim_end_not_found() {
let content = "### BH-01: First Claim\n";
let pos = find_claim_end(content, "BH-99");
assert!(pos.is_none());
}
#[test]
fn test_code_location_creation() {
let loc = CodeLocation {
file: PathBuf::from("src/main.rs"),
line: 42,
context: "fn main()".to_string(),
};
assert_eq!(loc.file, PathBuf::from("src/main.rs"));
assert_eq!(loc.line, 42);
assert_eq!(loc.context, "fn main()");
}
#[test]
fn test_spec_claim_creation() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test Claim".to_string(),
line: 10,
section_path: vec!["Section 1".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
};
assert_eq!(claim.id, "TEST-01");
assert_eq!(claim.status, ClaimStatus::Pending);
}
#[test]
fn test_parse_claim_header_short_prefix() {
let result = parse_claim_header("### X-1: Short");
assert!(result.is_some());
let (id, _) = result.unwrap();
assert_eq!(id, "X-1");
}
#[test]
fn test_parse_claim_header_long_prefix() {
let result = parse_claim_header("### ABCD-1234: Long");
assert!(result.is_some());
let (id, _) = result.unwrap();
assert_eq!(id, "ABCD-1234");
}
#[test]
fn test_parse_claim_header_too_long_prefix() {
let result = parse_claim_header("### ABCDE-01: Too Long");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_lowercase() {
let result = parse_claim_header("### abc-01: Lower");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_no_colon() {
let result = parse_claim_header("### BH-01 No Colon");
assert!(result.is_none());
}
#[test]
fn test_parse_claims_with_sections() {
let content = r#"
## Section One
### CB-001: First
## Section Two
### CB-002: Second
"#;
let claims = parse_claims(content);
assert_eq!(claims.len(), 2);
assert!(claims[0].section_path.contains(&"Section One".to_string()));
assert!(claims[1].section_path.contains(&"Section Two".to_string()));
}
#[test]
fn test_parse_claims_with_subsections() {
let content = r#"
## Main Section
### Sub Section
#### CB-001: Claim
"#;
let claims = parse_claims(content);
assert_eq!(claims.len(), 0); }
#[test]
fn test_parsed_spec_claims_for_section() {
let spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![
SpecClaim {
id: "BH-01".to_string(),
title: "Bug Hunt".to_string(),
line: 1,
section_path: vec!["Bug Hunting".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
},
SpecClaim {
id: "AUTH-01".to_string(),
title: "Auth Check".to_string(),
line: 10,
section_path: vec!["Authentication".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
},
],
original_content: String::new(),
};
let bh_claims = spec.claims_for_section("Bug");
assert_eq!(bh_claims.len(), 1);
assert_eq!(bh_claims[0].id, "BH-01");
let auth_claims = spec.claims_for_section("AUTH");
assert_eq!(auth_claims.len(), 1);
assert_eq!(auth_claims[0].id, "AUTH-01");
}
#[test]
fn test_claim_status_equality() {
assert_eq!(ClaimStatus::Verified, ClaimStatus::Verified);
assert_ne!(ClaimStatus::Verified, ClaimStatus::Failed);
}
#[test]
fn test_generate_status_block_verified() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Verified,
};
let findings: Vec<Finding> = vec![];
let block = generate_status_block(&claim, &findings);
assert!(block.contains("bug-hunter-status"));
assert!(block.contains("Verified"));
assert!(block.contains("None ✓"));
}
#[test]
fn test_generate_status_block_with_findings() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Warning,
};
let findings =
vec![Finding::new("F-001", "test.rs", 10, "Test finding")
.with_severity(FindingSeverity::Low)];
let block = generate_status_block(&claim, &findings);
assert!(block.contains("1 issue(s)"));
assert!(block.contains("F-001"));
}
#[test]
fn test_generate_status_block_with_implementations() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![CodeLocation {
file: PathBuf::from("src/lib.rs"),
line: 42,
context: "fn impl_func()".to_string(),
}],
findings: vec![],
status: ClaimStatus::Verified,
};
let findings: Vec<Finding> = vec![];
let block = generate_status_block(&claim, &findings);
assert!(block.contains("Implementations:"));
assert!(block.contains("src/lib.rs:42"));
}
#[test]
fn test_generate_status_block_many_findings() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Failed,
};
let findings: Vec<Finding> = (0..10)
.map(|i| Finding::new(format!("F-{:03}", i), "test.rs", i, format!("Finding {}", i)))
.collect();
let block = generate_status_block(&claim, &findings);
assert!(block.contains("10 issue(s)"));
assert!(block.contains("and 5 more"));
}
#[test]
fn test_update_with_findings_verified() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\nSome content.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![("BH-01".to_string(), vec![])];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
let updated = result.unwrap();
assert!(updated.contains("Verified"));
assert_eq!(spec.claims[0].status, ClaimStatus::Verified);
}
#[test]
fn test_update_with_findings_warning() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\nSome content.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![(
"BH-01".to_string(),
vec![
Finding::new("F-001", "test.rs", 1, "Low severity").with_severity(FindingSeverity::Low)
],
)];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
assert_eq!(spec.claims[0].status, ClaimStatus::Warning);
}
#[test]
fn test_update_with_findings_failed() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\nSome content.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![(
"BH-01".to_string(),
vec![Finding::new("F-001", "test.rs", 1, "Critical issue")
.with_severity(FindingSeverity::Critical)],
)];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
assert_eq!(spec.claims[0].status, ClaimStatus::Failed);
}
#[test]
fn test_update_with_findings_unknown_claim() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\nSome content.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![("BH-99".to_string(), vec![])];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
assert_eq!(spec.claims[0].status, ClaimStatus::Pending);
}
#[test]
fn test_parsed_spec_parse_nonexistent_file() {
let result = ParsedSpec::parse(Path::new("/nonexistent/file.md"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to read"));
}
#[test]
fn test_code_location_serialization() {
let loc = CodeLocation {
file: PathBuf::from("src/main.rs"),
line: 42,
context: "fn main()".to_string(),
};
let json = serde_json::to_string(&loc).unwrap();
let deserialized: CodeLocation = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.file, loc.file);
assert_eq!(deserialized.line, loc.line);
}
#[test]
fn test_spec_claim_serialization() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec!["Section".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Verified,
};
let json = serde_json::to_string(&claim).unwrap();
let deserialized: SpecClaim = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, claim.id);
assert_eq!(deserialized.status, ClaimStatus::Verified);
}
#[test]
fn test_parsed_spec_serialization() {
let spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![],
original_content: "# Test".to_string(),
};
let json = serde_json::to_string(&spec).unwrap();
let deserialized: ParsedSpec = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.path, spec.path);
}
#[test]
fn test_map_findings_to_claims_basic() {
let temp = std::env::temp_dir().join("test_spec_map");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
std::fs::write(
temp.join("src/lib.rs"),
"// Implements BH-01: mutation testing\nfn run_mutations() {}\n",
)
.unwrap();
let claims = vec![SpecClaim {
id: "BH-01".to_string(),
title: "Mutation Testing".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}];
let findings = vec![Finding::new("F-001", temp.join("src/lib.rs"), 2, "Pattern: unwrap")
.with_severity(FindingSeverity::Medium)];
let mapping = map_findings_to_claims(&claims, &findings, &temp);
assert!(mapping.contains_key("BH-01"));
let claim_findings = &mapping["BH-01"];
assert_eq!(claim_findings.len(), 1, "Finding near implementation should be mapped");
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_map_findings_to_claims_no_match() {
let temp = std::env::temp_dir().join("test_spec_map_nomatch");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
std::fs::write(temp.join("src/lib.rs"), "// BH-01 implemented here\nfn impl_bh01() {}\n")
.unwrap();
let claims = vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}];
let findings = vec![Finding::new("F-001", PathBuf::from("src/other.rs"), 100, "Pattern: TODO")
.with_severity(FindingSeverity::Low)];
let mapping = map_findings_to_claims(&claims, &findings, &temp);
let claim_findings = &mapping["BH-01"];
assert!(claim_findings.is_empty(), "Finding in different file should not be mapped");
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_map_findings_to_claims_far_line() {
let temp = std::env::temp_dir().join("test_spec_map_far");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
let mut content = "// BH-01 implemented here\n".to_string();
for i in 0..100 {
content.push_str(&format!("fn line_{}() {{}}\n", i));
}
std::fs::write(temp.join("src/lib.rs"), &content).unwrap();
let claims = vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}];
let findings = vec![Finding::new("F-001", temp.join("src/lib.rs"), 90, "Pattern: HACK")
.with_severity(FindingSeverity::Medium)];
let mapping = map_findings_to_claims(&claims, &findings, &temp);
let claim_findings = &mapping["BH-01"];
assert!(
claim_findings.is_empty(),
"Finding >50 lines from implementation should not be mapped"
);
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_find_implementations_basic() {
let temp = std::env::temp_dir().join("test_spec_find_impl");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
std::fs::write(
temp.join("src/lib.rs"),
"/// Implements AUTH-01 token validation\nfn validate_token() {}\n",
)
.unwrap();
let claim = SpecClaim {
id: "AUTH-01".to_string(),
title: "Token Validation".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
};
let impls = find_implementations(&claim, &temp);
assert!(!impls.is_empty(), "Should find implementation referencing AUTH-01");
assert_eq!(impls[0].line, 1);
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_find_implementations_no_match() {
let temp = std::env::temp_dir().join("test_spec_find_impl_none");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
std::fs::write(temp.join("src/lib.rs"), "fn main() {}\n").unwrap();
let claim = SpecClaim {
id: "NONEXIST-99".to_string(),
title: "Missing".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
};
let impls = find_implementations(&claim, &temp);
assert!(impls.is_empty());
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_parse_claim_header_empty_suffix() {
let result = parse_claim_header("### BH-: Missing Digits");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_too_long_suffix() {
let result = parse_claim_header("### BH-12345: Too Long");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_mixed_case_prefix() {
let result = parse_claim_header("### Bh-01: Mixed");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_no_dash() {
let result = parse_claim_header("### NODASH01: Missing Dash");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_empty_prefix() {
let result = parse_claim_header("### -01: No Prefix");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_digits_in_prefix() {
let result = parse_claim_header("### B1-01: Digit in Prefix");
assert!(result.is_none());
}
#[test]
fn test_parse_claim_header_leading_hashes_stripped() {
let result = parse_claim_header("### CB-020: Valid");
assert!(result.is_some());
let (id, title) = result.unwrap();
assert_eq!(id, "CB-020");
assert_eq!(title, "Valid");
}
#[test]
fn test_parse_claims_section_hierarchy_with_subsections() {
let content = r#"
## Parent Section
### Sub Section
### CB-001: Claim Under Sub
## Another Section
### CB-002: Claim Under Another
"#;
let claims = parse_claims(content);
assert_eq!(claims.len(), 2);
assert!(claims[0].section_path.contains(&"Parent Section".to_string()));
assert!(claims[1].section_path.contains(&"Another Section".to_string()));
}
#[test]
fn test_parse_claims_empty_content() {
let claims = parse_claims("");
assert!(claims.is_empty());
}
#[test]
fn test_parse_claims_no_matching_headers() {
let content = "## Section\n\n### Just A Title\n\nSome text.\n";
let claims = parse_claims(content);
assert!(claims.is_empty());
}
#[test]
fn test_parse_claims_line_numbers() {
let content = "## Section\n\n### BH-01: First\n\nText\n\n### BH-02: Second\n";
let claims = parse_claims(content);
assert_eq!(claims.len(), 2);
assert_eq!(claims[0].line, 3);
assert_eq!(claims[1].line, 7);
}
#[test]
fn test_claims_for_section_match_by_title() {
let spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "CB-001".to_string(),
title: "Memory Safety Validation".to_string(),
line: 1,
section_path: vec!["Other".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: String::new(),
};
let results = spec.claims_for_section("Memory Safety");
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "CB-001");
}
#[test]
fn test_claims_for_section_no_match() {
let spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "CB-001".to_string(),
title: "Bug Hunt".to_string(),
line: 1,
section_path: vec!["Section A".to_string()],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: String::new(),
};
let results = spec.claims_for_section("Nonexistent");
assert!(results.is_empty());
}
#[test]
fn test_generate_status_block_failed_status() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Failed,
};
let findings = vec![
Finding::new("F-001", "a.rs", 10, "Critical bug").with_severity(FindingSeverity::Critical),
Finding::new("F-002", "b.rs", 20, "Another bug").with_severity(FindingSeverity::High),
];
let block = generate_status_block(&claim, &findings);
assert!(block.contains("Failed"));
assert!(block.contains("2 issue(s)"));
assert!(block.contains("F-001"));
assert!(block.contains("F-002"));
}
#[test]
fn test_generate_status_block_exactly_five_findings() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Warning,
};
let findings: Vec<Finding> = (0..5)
.map(|i| Finding::new(format!("F-{:03}", i), "test.rs", i, format!("Finding {}", i)))
.collect();
let block = generate_status_block(&claim, &findings);
assert!(block.contains("5 issue(s)"));
assert!(!block.contains("more"));
}
#[test]
fn test_generate_status_block_six_findings() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Warning,
};
let findings: Vec<Finding> = (0..6)
.map(|i| Finding::new(format!("F-{:03}", i), "test.rs", i, format!("Finding {}", i)))
.collect();
let block = generate_status_block(&claim, &findings);
assert!(block.contains("6 issue(s)"));
assert!(block.contains("and 1 more"));
}
#[test]
fn test_generate_status_block_multiple_implementations() {
let claim = SpecClaim {
id: "TEST-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![
CodeLocation {
file: PathBuf::from("src/a.rs"),
line: 10,
context: "fn first()".to_string(),
},
CodeLocation {
file: PathBuf::from("src/b.rs"),
line: 20,
context: "fn second()".to_string(),
},
],
findings: vec![],
status: ClaimStatus::Verified,
};
let block = generate_status_block(&claim, &[]);
assert!(block.contains("src/a.rs:10"));
assert!(block.contains("src/b.rs:20"));
assert!(block.contains("fn first()"));
assert!(block.contains("fn second()"));
}
#[test]
fn test_remove_existing_status_blocks_multiple_blocks() {
let content = r#"### BH-01: Claim 1
<!-- bug-hunter-status -->
Status for claim 1
<!-- /bug-hunter-status -->
Text between.
### BH-02: Claim 2
<!-- bug-hunter-status -->
Status for claim 2
<!-- /bug-hunter-status -->
Final text.
"#;
let result = remove_existing_status_blocks(content);
assert!(!result.contains("bug-hunter-status"));
assert!(!result.contains("Status for claim"));
assert!(result.contains("Text between."));
assert!(result.contains("Final text."));
}
#[test]
fn test_remove_existing_status_blocks_preserves_content_lines() {
let content = "Line 1\nLine 2\nLine 3\n";
let result = remove_existing_status_blocks(content);
assert_eq!(result, "Line 1\nLine 2\nLine 3\n");
}
#[test]
fn test_find_claim_end_returns_correct_byte_position() {
let content = "### BH-01: Test\nNext line here.\n";
let pos = find_claim_end(content, "BH-01").unwrap();
assert_eq!(pos, 16);
}
#[test]
fn test_find_claim_end_multiple_claims() {
let content = "### BH-01: First\n### BH-02: Second\n";
let pos1 = find_claim_end(content, "BH-01").unwrap();
let pos2 = find_claim_end(content, "BH-02").unwrap();
assert!(pos1 < pos2);
}
#[test]
fn test_update_with_findings_high_severity_triggers_failed() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\nSome content.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![(
"BH-01".to_string(),
vec![Finding::new("F-001", "test.rs", 1, "High issue").with_severity(FindingSeverity::High)],
)];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
assert_eq!(spec.claims[0].status, ClaimStatus::Failed);
}
#[test]
fn test_update_with_findings_multiple_claims() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![
SpecClaim {
id: "BH-01".to_string(),
title: "First Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
},
SpecClaim {
id: "BH-02".to_string(),
title: "Second Claim".to_string(),
line: 5,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
},
],
original_content:
"### BH-01: First Claim\n\nText.\n\n### BH-02: Second Claim\n\nMore text.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![
("BH-01".to_string(), vec![]),
(
"BH-02".to_string(),
vec![Finding::new("F-001", "test.rs", 1, "Med severity")
.with_severity(FindingSeverity::Medium)],
),
];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
assert_eq!(spec.claims[0].status, ClaimStatus::Verified);
assert_eq!(spec.claims[1].status, ClaimStatus::Warning);
}
#[test]
fn test_parsed_spec_parse_valid_file() {
let temp_dir = std::env::temp_dir().join("test_spec_parse_valid");
let _ = std::fs::create_dir_all(&temp_dir);
let spec_file = temp_dir.join("spec.md");
std::fs::write(
&spec_file,
"## Section\n\n### BH-01: First Claim\n\nDetails.\n\n### BH-02: Second Claim\n",
)
.unwrap();
let result = ParsedSpec::parse(&spec_file);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.claims.len(), 2);
assert_eq!(spec.claims[0].id, "BH-01");
assert_eq!(spec.claims[1].id, "BH-02");
assert_eq!(spec.path, spec_file);
assert!(spec.original_content.contains("## Section"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_write_updated_creates_backup_and_writes() {
let temp_dir = std::env::temp_dir().join("test_spec_write_updated");
let _ = std::fs::remove_dir_all(&temp_dir);
let _ = std::fs::create_dir_all(&temp_dir);
let spec_file = temp_dir.join("spec.md");
let original = "### BH-01: Test\n\nOriginal content.\n";
std::fs::write(&spec_file, original).unwrap();
let spec = ParsedSpec {
path: spec_file.clone(),
claims: vec![],
original_content: original.to_string(),
};
let updated = "### BH-01: Test\n\nUpdated content.\n";
let result = spec.write_updated(updated);
assert!(result.is_ok());
let backup_path = spec_file.with_extension("md.bak");
assert!(backup_path.exists());
let backup_content = std::fs::read_to_string(&backup_path).unwrap();
assert_eq!(backup_content, original);
let new_content = std::fs::read_to_string(&spec_file).unwrap();
assert_eq!(new_content, updated);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_write_updated_fails_on_invalid_path() {
let spec = ParsedSpec {
path: PathBuf::from("/nonexistent/dir/spec.md"),
claims: vec![],
original_content: String::new(),
};
let result = spec.write_updated("content");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to create backup"));
}
#[test]
fn test_find_implementations_long_context_truncated() {
let temp = std::env::temp_dir().join("test_spec_impl_trunc");
let _ = std::fs::remove_dir_all(&temp);
let _ = std::fs::create_dir_all(temp.join("src"));
let long_line = format!("// CB-001 {}", "x".repeat(200));
std::fs::write(temp.join("src/lib.rs"), &long_line).unwrap();
let claim = SpecClaim {
id: "CB-001".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
};
let impls = find_implementations(&claim, &temp);
assert!(!impls.is_empty());
assert!(impls[0].context.len() <= 60);
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn test_find_implementations_nonexistent_path() {
let claim = SpecClaim {
id: "BH-01".to_string(),
title: "Test".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
};
let impls = find_implementations(&claim, Path::new("/nonexistent/project"));
assert!(impls.is_empty());
}
#[test]
fn test_update_with_findings_replaces_existing_status() {
let mut spec = ParsedSpec {
path: PathBuf::from("test.md"),
claims: vec![SpecClaim {
id: "BH-01".to_string(),
title: "Test Claim".to_string(),
line: 1,
section_path: vec![],
implementations: vec![],
findings: vec![],
status: ClaimStatus::Pending,
}],
original_content: "### BH-01: Test Claim\n\n<!-- bug-hunter-status -->\nOld status\n<!-- /bug-hunter-status -->\n\nContent after.\n".to_string(),
};
let findings: Vec<(String, Vec<Finding>)> = vec![("BH-01".to_string(), vec![])];
let result = spec.update_with_findings(&findings);
assert!(result.is_ok());
let updated = result.unwrap();
assert!(updated.contains("Verified"));
assert!(!updated.contains("Old status"));
}