use std::collections::HashSet;
use super::*;
use crate::content::angles::EvidenceType;
fn neighbor(id: i64, title: &str, snippet: &str) -> NeighborContent {
NeighborContent {
node_id: id,
note_title: title.to_string(),
heading_path: None,
snippet: snippet.to_string(),
}
}
#[test]
fn pre_filter_catches_percentages() {
let neighbors = vec![neighbor(1, "Growth", "Revenue grew 45% last quarter")];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 1);
assert!(candidates[0].text.contains("45%"));
}
#[test]
fn pre_filter_catches_dollars() {
let neighbors = vec![neighbor(1, "Revenue", "Earned $1,200 in MRR")];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 1);
assert!(candidates[0].text.contains("$1,200"));
}
#[test]
fn pre_filter_catches_multipliers() {
let neighbors = vec![neighbor(1, "Perf", "Achieved 3.5x improvement")];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 1);
assert!(candidates[0].text.contains("3.5x"));
}
#[test]
fn pre_filter_catches_iso_dates() {
let neighbors = vec![neighbor(1, "Launch", "Launched on 2025-06-15")];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 1);
assert!(candidates[0].text.contains("2025-06-15"));
}
#[test]
fn pre_filter_catches_counts() {
let neighbors = vec![neighbor(1, "Users", "150 users migrated to v2")];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 1);
assert!(candidates[0].text.contains("150 users"));
}
#[test]
fn pre_filter_empty_on_plain_text() {
let neighbors = vec![neighbor(1, "Note", "No numbers here at all")];
let candidates = pre_filter_data_points(&neighbors);
assert!(candidates.is_empty());
}
fn evidence_item(
etype: EvidenceType,
node_id: i64,
confidence: f64,
citation: &str,
) -> EvidenceItem {
EvidenceItem {
evidence_type: etype,
citation_text: citation.to_string(),
source_node_id: node_id,
source_note_title: "Note".to_string(),
source_heading_path: None,
confidence,
}
}
#[test]
fn validate_rejects_invalid_node_ids() {
let accepted: HashSet<i64> = [1, 2].into_iter().collect();
let evidence = vec![
evidence_item(EvidenceType::DataPoint, 1, 0.8, "valid"),
evidence_item(EvidenceType::DataPoint, 99, 0.9, "invalid node"),
];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 1);
assert_eq!(result[0].source_node_id, 1);
}
#[test]
fn validate_truncates_long_citations() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let long_citation = "a".repeat(200);
let evidence = vec![evidence_item(
EvidenceType::DataPoint,
1,
0.8,
&long_citation,
)];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 1);
assert!(result[0].citation_text.len() <= MAX_CITATION_CHARS);
assert!(result[0].citation_text.ends_with("..."));
}
#[test]
fn validate_rejects_low_confidence() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let evidence = vec![evidence_item(EvidenceType::DataPoint, 1, 0.05, "low conf")];
let result = validate_evidence(evidence, &accepted);
assert!(result.is_empty());
}
#[test]
fn validate_deduplicates() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let evidence = vec![
evidence_item(EvidenceType::DataPoint, 1, 0.6, "first"),
evidence_item(EvidenceType::DataPoint, 1, 0.9, "second (higher)"),
];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 1);
assert_eq!(result[0].citation_text, "second (higher)");
}
#[test]
fn parse_evidence_json() {
let json = r#"[
{
"evidence_type": "data_point",
"citation_text": "45% growth",
"source_node_id": 1,
"confidence": 0.8
}
]"#;
let neighbors = vec![neighbor(1, "Growth", "Revenue grew 45% last quarter")];
let result = parse_evidence_response(json, &neighbors).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].evidence_type, EvidenceType::DataPoint);
assert_eq!(result[0].source_note_title, "Growth");
}
#[test]
fn parse_evidence_json_in_code_block() {
let text = "Here is the evidence:\n```json\n[\n{\"evidence_type\": \"aha_moment\", \
\"citation_text\": \"surprise\", \"source_node_id\": 2, \"confidence\": 0.7}\n]\n```";
let neighbors = vec![neighbor(2, "Insights", "A surprising finding")];
let result = parse_evidence_response(text, &neighbors).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].evidence_type, EvidenceType::AhaMoment);
}
#[test]
fn parse_evidence_empty_array() {
let result = parse_evidence_response("[]", &[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn truncate_short_string_is_noop() {
assert_eq!(truncate_at_char_boundary("hello", 10), "hello");
}
#[test]
fn truncate_at_exact_length() {
assert_eq!(truncate_at_char_boundary("hello", 5), "hello");
}
#[test]
fn truncate_ascii_string() {
assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
}
#[test]
fn truncate_multibyte_respects_boundary() {
let s = "ab\u{1F600}cd"; let result = truncate_at_char_boundary(s, 3);
assert_eq!(result, "ab");
}
#[test]
fn truncate_two_byte_chars() {
let s = "\u{00E9}\u{00E9}\u{00E9}"; let result = truncate_at_char_boundary(s, 3);
assert_eq!(result, "\u{00E9}");
}
#[test]
fn truncate_empty_string() {
assert_eq!(truncate_at_char_boundary("", 5), "");
}
#[test]
fn truncate_zero_max_len() {
assert_eq!(truncate_at_char_boundary("hello", 0), "");
}
#[test]
fn extract_json_code_block_basic() {
let text = "Here:\n```json\n[1, 2, 3]\n```\nDone.";
assert_eq!(extract_json_from_code_block(text), Some("[1, 2, 3]"));
}
#[test]
fn extract_json_code_block_no_marker() {
assert!(extract_json_from_code_block("no code block here").is_none());
}
#[test]
fn extract_json_code_block_no_end_marker() {
let text = "```json\n[1, 2, 3]\nno closing";
assert!(extract_json_from_code_block(text).is_none());
}
#[test]
fn parse_evidence_fallback_embedded_array() {
let text = "Some preamble text [
{\"evidence_type\": \"data_point\", \"citation_text\": \"test\", \"source_node_id\": 1, \"confidence\": 0.5}
] and trailing text";
let neighbors = vec![neighbor(1, "Note", "snippet")];
let result = parse_evidence_response(text, &neighbors).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].evidence_type, EvidenceType::DataPoint);
}
#[test]
fn parse_evidence_unparseable_returns_empty() {
let result = parse_evidence_response("totally not json", &[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_evidence_unknown_type_filtered_out() {
let json = r#"[
{"evidence_type": "unknown_type", "citation_text": "test", "source_node_id": 1, "confidence": 0.5}
]"#;
let neighbors = vec![neighbor(1, "Note", "snippet")];
let result = parse_evidence_response(json, &neighbors).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_evidence_unknown_node_id_still_parsed() {
let json = r#"[
{"evidence_type": "contradiction", "citation_text": "x", "source_node_id": 999, "confidence": 0.7}
]"#;
let neighbors = vec![neighbor(1, "Note", "snippet")];
let result = parse_evidence_response(json, &neighbors).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].source_note_title, "");
}
#[test]
fn parse_evidence_default_confidence() {
let json = r#"[{"evidence_type": "aha_moment", "citation_text": "test", "source_node_id": 1}]"#;
let neighbors = vec![neighbor(1, "Note", "snippet")];
let result = parse_evidence_response(json, &neighbors).unwrap();
assert_eq!(result.len(), 1);
assert!((result[0].confidence - 0.5).abs() < f64::EPSILON);
}
#[test]
fn build_extraction_prompt_includes_topic() {
let neighbors = vec![neighbor(1, "Note", "snippet text")];
let prompt = build_extraction_prompt("AI safety", &neighbors, &[]);
assert!(prompt.contains("Topic: AI safety"));
assert!(prompt.contains("snippet text"));
assert!(prompt.contains("node_id: 1"));
}
#[test]
fn build_extraction_prompt_includes_candidates() {
let neighbors = vec![neighbor(1, "Note", "snippet")];
let candidates = vec![CandidateDataPoint {
text: "grew 45%".to_string(),
source_node_id: 1,
source_note_title: "Note".to_string(),
}];
let prompt = build_extraction_prompt("Growth", &neighbors, &candidates);
assert!(prompt.contains("Candidate data points"));
assert!(prompt.contains("grew 45%"));
}
#[test]
fn build_extraction_prompt_no_candidates_section() {
let neighbors = vec![neighbor(1, "Note", "snippet")];
let prompt = build_extraction_prompt("Topic", &neighbors, &[]);
assert!(!prompt.contains("Candidate data points"));
}
#[test]
fn validate_dedup_keeps_first_when_equal_confidence() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let evidence = vec![
evidence_item(EvidenceType::DataPoint, 1, 0.7, "first"),
evidence_item(EvidenceType::DataPoint, 1, 0.7, "second"),
];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 1);
assert_eq!(result[0].citation_text, "first");
}
#[test]
fn validate_different_types_not_deduped() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let evidence = vec![
evidence_item(EvidenceType::DataPoint, 1, 0.8, "data"),
evidence_item(EvidenceType::Contradiction, 1, 0.8, "contra"),
];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 2);
}
#[test]
fn validate_confidence_exactly_at_floor() {
let accepted: HashSet<i64> = [1].into_iter().collect();
let evidence = vec![evidence_item(EvidenceType::DataPoint, 1, 0.1, "at floor")];
let result = validate_evidence(evidence, &accepted);
assert_eq!(result.len(), 1);
}
#[test]
fn parse_evidence_preserves_heading_path() {
let json = r#"[{"evidence_type": "data_point", "citation_text": "x", "source_node_id": 1, "confidence": 0.8}]"#;
let neighbors = vec![NeighborContent {
node_id: 1,
note_title: "Note".to_string(),
heading_path: Some("# Heading > ## Sub".to_string()),
snippet: "snippet".to_string(),
}];
let result = parse_evidence_response(json, &neighbors).unwrap();
assert_eq!(
result[0].source_heading_path.as_deref(),
Some("# Heading > ## Sub")
);
}
#[test]
fn pre_filter_multiple_matches_in_one_snippet() {
let neighbors = vec![neighbor(
1,
"Stats",
"Revenue grew 45% and we gained 150 users in Q2",
)];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 2);
}
#[test]
fn pre_filter_multiple_neighbors() {
let neighbors = vec![
neighbor(1, "A", "Growth was 10x"),
neighbor(2, "B", "Nothing to see"),
neighbor(3, "C", "Date was 2025-01-01"),
];
let candidates = pre_filter_data_points(&neighbors);
assert_eq!(candidates.len(), 2);
assert_eq!(candidates[0].source_node_id, 1);
assert_eq!(candidates[1].source_node_id, 3);
}