#[cfg(test)]
mod phase3_integration_tests {
use crate::config::enhanced_models::*;
use crate::models::models::{PageMetrics, ProcessedDocument};
use crate::scoring::{
ProfileCompiler, ProfileAwareScorer, ContentValidator, Phase3ScoringSystem,
CompiledProfile, ScoringResult, ContentValidationResult, ViolationSeverity,
};
use std::collections::HashMap;
struct Phase3TestData {
profile_config: ProfileConfig,
metrics: PageMetrics,
document: ProcessedDocument,
}
impl Phase3TestData {
fn new() -> Self {
Self {
profile_config: Self::create_test_profile(),
metrics: Self::create_test_metrics(),
document: Self::create_test_document(),
}
}
fn create_test_profile() -> ProfileConfig {
ProfileConfig {
name: "test_profile".to_string(),
description: Some("Test profile for Phase 3 validation".to_string()),
target_content_type: ContentType::Article,
metric_weights: Self::create_metric_weights(),
category_weights: Self::create_category_weights(),
global_penalties: vec![
GlobalPenalty {
id: "low_word_count".to_string(),
description: "Penalty for insufficient content".to_string(),
trigger_condition: PenaltyTrigger::MetricThreshold {
metric_name: "word_count".to_string(),
operator: ComparisonOperator::LessThan,
threshold: MetricValue::Integer(300),
},
penalty_amount: PenaltyAmount::Fixed(20.0),
severity: PenaltySeverity::High,
applies_to: vec![MetricCategory::Content],
max_applications: Some(1),
},
],
global_bonuses: vec![
GlobalBonus {
id: "excellent_readability".to_string(),
description: "Bonus for excellent readability".to_string(),
trigger_condition: BonusTrigger::MetricThreshold {
metric_name: "readability_score".to_string(),
operator: ComparisonOperator::GreaterThan,
threshold: MetricValue::Float(80.0),
},
bonus_amount: BonusAmount::Fixed(10.0),
applies_to: vec![MetricCategory::Content],
max_applications: Some(1),
},
],
content_expectations: Some(ContentExpectations {
word_count: Some(WordCountExpectation {
minimum: 300,
optimal_range: (500, 2000),
maximum_useful: Some(5000),
penalty_curve: PenaltyCurve::Linear,
}),
heading_structure: Some(HeadingExpectation {
require_h1: true,
minimum_headings: 3,
maximum_heading_depth: 4,
logical_hierarchy: true,
}),
media_requirements: Some(MediaExpectation {
minimum_images: 1,
alt_text_coverage: 0.9,
image_to_text_ratio: Some((0.01, 0.1)),
require_video: Some(false),
require_audio: Some(false),
}),
technical_requirements: Some(TechnicalExpectation {
ssl_required: true,
required_meta_tags: vec!["description".to_string(), "viewport".to_string()],
}),
seo_requirements: Some(SeoExpectation {
title_length_range: Some((30, 60)),
meta_description_required: true,
meta_description_length_range: Some((120, 160)),
canonical_url_required: true,
structured_data_required: false,
open_graph_required: false,
}),
}),
}
}
fn create_metric_weights() -> HashMap<String, f32> {
let mut weights = HashMap::new();
weights.insert("word_count".to_string(), 1.5);
weights.insert("readability_score".to_string(), 2.0);
weights.insert("title_len".to_string(), 1.2);
weights.insert("meta_desc_len".to_string(), 1.1);
weights.insert("heading_count".to_string(), 1.0);
weights.insert("image_count".to_string(), 0.8);
weights
}
fn create_category_weights() -> HashMap<MetricCategory, f32> {
let mut weights = HashMap::new();
weights.insert(MetricCategory::Content, 3.0);
weights.insert(MetricCategory::Structure, 2.5);
weights.insert(MetricCategory::SEO, 2.0);
weights.insert(MetricCategory::Technical, 1.5);
weights.insert(MetricCategory::Accessibility, 1.8);
weights.insert(MetricCategory::Media, 1.0);
weights
}
fn create_test_metrics() -> PageMetrics {
use crate::models::models::*;
PageMetrics {
html_analysis: HtmlAnalysis {
content: ContentMetrics {
word_count: 800,
paragraph_count: 12,
sentence_count: 45,
readability_score: Some(75.5),
reading_time_minutes: Some(4),
text_density: Some(0.65),
avg_words_per_sentence: Some(17.8),
avg_sentences_per_paragraph: Some(3.75),
},
structure: StructureMetrics {
heading_count: 5,
list_count: 3,
table_count: 1,
nav_count: 1,
main_count: 1,
aside_count: 2,
footer_count: 1,
header_count: 1,
},
media: MediaMetrics {
image_count: 4,
video_count: 0,
audio_count: 0,
interactive_count: 1,
},
seo: SeoMetrics {
title_len: Some(45),
meta_desc_len: Some(145),
h1_count: 1,
canonical_url: Some("https://example.com/article".to_string()),
meta_keywords: None,
og_title: Some("Test Article".to_string()),
og_description: Some("A test article for validation".to_string()),
og_image: Some("https://example.com/image.jpg".to_string()),
schema_markup_types: vec!["Article".to_string()],
},
accessibility: AccessibilityMetrics {
alt_text_coverage: 0.95,
aria_label_coverage: 0.8,
color_contrast_issues: 1,
keyboard_navigation_score: Some(85.0),
screen_reader_score: Some(90.0),
},
technical: TechnicalMetrics {
page_size_kb: Some(245),
load_time_ms: Some(1200),
dom_elements: Some(156),
critical_css_coverage: Some(0.85),
js_errors: Some(0),
console_warnings: Some(2),
},
links_social: LinksSocialMetrics {
internal_links: 8,
external_links: 3,
social_shares: Some(15),
backlink_estimate: Some(42),
},
mobile_usability: MobileUsabilityMetrics {
mobile_friendly: Some(true),
viewport_configured: Some(true),
touch_targets_sized: Some(true),
responsive_images: Some(0.9),
},
performance: PerformanceMetrics {
core_web_vitals_score: Some(85.0),
lighthouse_performance: Some(88),
first_contentful_paint_ms: Some(800),
largest_contentful_paint_ms: Some(1100),
cumulative_layout_shift: Some(0.05),
first_input_delay_ms: Some(45),
},
language_nlp: LanguageNlpMetrics {
detected_language: Some("en".to_string()),
language_confidence: Some(0.95),
sentiment_score: Some(0.2),
topic_categories: vec!["Technology".to_string(), "Web Development".to_string()],
keyword_density: Some(0.02),
duplicate_content_percentage: Some(0.05),
},
},
}
}
fn create_test_document() -> ProcessedDocument {
ProcessedDocument {
title: Some("Test Article: Phase 3 Validation".to_string()),
content: "This is a comprehensive test article designed to validate the Phase 3 profile-aware scoring engine. The article contains multiple paragraphs with structured content, headings, and rich media elements.".to_string(),
headings: vec![
"Test Article: Phase 3 Validation".to_string(),
"Introduction".to_string(),
"Methodology".to_string(),
"Results".to_string(),
"Conclusion".to_string(),
],
links: vec![
"https://example.com/internal".to_string(),
"https://external-site.com/reference".to_string(),
],
images: vec![
"https://example.com/chart.png".to_string(),
"https://example.com/diagram.jpg".to_string(),
"https://example.com/photo.webp".to_string(),
"https://example.com/illustration.svg".to_string(),
],
meta_description: Some("A comprehensive test article for Phase 3 profile-aware scoring engine validation with detailed content analysis.".to_string()),
word_count: 800,
reading_time: 4,
language: Some("en".to_string()),
}
}
}
#[test]
fn test_profile_compiler_basic_functionality() {
let test_data = Phase3TestData::new();
let compiler = ProfileCompiler::new();
let compiled_profile = compiler.compile_profile(&test_data.profile_config);
assert!(compiled_profile.is_ok(), "Profile compilation should succeed");
let compiled = compiled_profile.unwrap();
assert_eq!(compiled.name, "test_profile");
assert_eq!(compiled.target_content_type, ContentType::Article);
assert!(!compiled.metric_rules.is_empty(), "Should have compiled metric rules");
assert!(!compiled.global_penalties.is_empty(), "Should have compiled penalties");
assert!(!compiled.global_bonuses.is_empty(), "Should have compiled bonuses");
}
#[test]
fn test_profile_compiler_metric_rules() {
let test_data = Phase3TestData::new();
let compiler = ProfileCompiler::new();
let compiled = compiler.compile_profile(&test_data.profile_config).unwrap();
let word_count_rule = compiled.metric_rules.iter()
.find(|rule| rule.metric_name == "word_count");
assert!(word_count_rule.is_some(), "Should have word count rule");
let word_count_rule = word_count_rule.unwrap();
assert_eq!(word_count_rule.weight, 1.5);
assert_eq!(word_count_rule.category, MetricCategory::Content);
}
#[test]
fn test_profile_aware_scorer_basic_scoring() {
let test_data = Phase3TestData::new();
let compiler = ProfileCompiler::new();
let scorer = ProfileAwareScorer::new();
let compiled_profile = compiler.compile_profile(&test_data.profile_config).unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
scorer.calculate_score(&compiled_profile, &test_data.metrics, &test_data.document).await
});
assert!(result.is_ok(), "Scoring should succeed");
let scoring_result = result.unwrap();
assert!(scoring_result.final_score >= 0.0 && scoring_result.final_score <= 100.0);
assert!(!scoring_result.category_scores.is_empty(), "Should have category scores");
assert!(scoring_result.metrics_processed > 0, "Should have processed metrics");
}
#[test]
fn test_profile_aware_scorer_category_scoring() {
let test_data = Phase3TestData::new();
let compiler = ProfileCompiler::new();
let scorer = ProfileAwareScorer::new();
let compiled_profile = compiler.compile_profile(&test_data.profile_config).unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
scorer.calculate_score(&compiled_profile, &test_data.metrics, &test_data.document).await
}).unwrap();
assert!(result.category_scores.contains_key(&MetricCategory::Content));
assert!(result.category_scores.contains_key(&MetricCategory::Structure));
assert!(result.category_scores.contains_key(&MetricCategory::SEO));
let content_score = result.category_scores.get(&MetricCategory::Content).unwrap();
assert!(content_score.raw_score > 0.0);
assert_eq!(content_score.weight, 3.0);
}
#[test]
fn test_content_validator_basic_functionality() {
let test_data = Phase3TestData::new();
let validator = ContentValidator::new();
let expectations = test_data.profile_config.content_expectations.as_ref().unwrap();
let result = validator.validate_content_expectations(
expectations,
&test_data.metrics,
&test_data.document
);
assert!(result.is_ok(), "Content validation should succeed");
let validation_result = result.unwrap();
assert!(validation_result.compliance_score >= 0.0 && validation_result.compliance_score <= 100.0);
assert!(validation_result.compliance_score > 80.0, "Should have high compliance with good test data");
}
#[test]
fn test_content_validator_word_count_validation() {
let mut test_data = Phase3TestData::new();
let validator = ContentValidator::new();
test_data.metrics.html_analysis.content.word_count = 200;
let expectations = test_data.profile_config.content_expectations.as_ref().unwrap();
let result = validator.validate_content_expectations(
expectations,
&test_data.metrics,
&test_data.document
).unwrap();
let has_word_count_violation = result.violations.iter().any(|v| {
matches!(v, crate::scoring::ContentViolation::InsufficientWordCount { .. })
});
assert!(has_word_count_violation, "Should detect insufficient word count");
assert!(!result.penalties.is_empty(), "Should have penalties for violations");
assert!(result.compliance_score < 80.0, "Compliance should be reduced due to violations");
}
#[test]
fn test_content_validator_heading_structure() {
let mut test_data = Phase3TestData::new();
let validator = ContentValidator::new();
test_data.metrics.html_analysis.structure.heading_count = 1;
let expectations = test_data.profile_config.content_expectations.as_ref().unwrap();
let result = validator.validate_content_expectations(
expectations,
&test_data.metrics,
&test_data.document
).unwrap();
let has_heading_violation = result.violations.iter().any(|v| {
matches!(v, crate::scoring::ContentViolation::InvalidHeadingStructure { .. })
});
assert!(has_heading_violation, "Should detect heading structure issues");
}
#[test]
fn test_phase3_system_complete_analysis() {
let test_data = Phase3TestData::new();
let system = Phase3ScoringSystem::new();
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
});
assert!(result.is_ok(), "Complete analysis should succeed");
let analysis_result = result.unwrap();
assert!(!analysis_result.compiled_profile.metric_rules.is_empty());
assert!(analysis_result.scoring_result.final_score > 0.0);
assert!(analysis_result.content_validation.is_some());
let content_validation = analysis_result.content_validation.unwrap();
assert!(content_validation.compliance_score >= 0.0);
}
#[test]
fn test_penalty_application() {
let mut test_data = Phase3TestData::new();
let system = Phase3ScoringSystem::new();
test_data.metrics.html_analysis.content.word_count = 250;
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
}).unwrap();
assert!(!result.scoring_result.applied_penalties.is_empty(), "Should have applied penalties");
let word_count_penalty = result.scoring_result.applied_penalties.iter()
.find(|p| p.penalty_id == "low_word_count");
assert!(word_count_penalty.is_some(), "Should have low word count penalty");
let penalty = word_count_penalty.unwrap();
assert_eq!(penalty.amount, 20.0);
assert_eq!(penalty.reason, "Penalty for insufficient content");
}
#[test]
fn test_bonus_application() {
let mut test_data = Phase3TestData::new();
let system = Phase3ScoringSystem::new();
test_data.metrics.html_analysis.content.readability_score = Some(85.0);
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
}).unwrap();
assert!(!result.scoring_result.applied_bonuses.is_empty(), "Should have applied bonuses");
let readability_bonus = result.scoring_result.applied_bonuses.iter()
.find(|b| b.bonus_id == "excellent_readability");
assert!(readability_bonus.is_some(), "Should have excellent readability bonus");
let bonus = readability_bonus.unwrap();
assert_eq!(bonus.amount, 10.0);
assert_eq!(bonus.reason, "Bonus for excellent readability");
}
#[test]
fn test_performance_characteristics() {
let test_data = Phase3TestData::new();
let system = Phase3ScoringSystem::new();
let start_time = std::time::Instant::now();
let rt = tokio::runtime::Runtime::new().unwrap();
for _ in 0..10 {
let result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
});
assert!(result.is_ok(), "All scoring operations should succeed");
}
let elapsed = start_time.elapsed();
assert!(elapsed.as_millis() < 1000, "Performance should be acceptable: {:?}", elapsed);
}
#[test]
fn test_caching_effectiveness() {
let test_data = Phase3TestData::new();
let compiler = ProfileCompiler::new();
let scorer = ProfileAwareScorer::new();
let compiled_profile = compiler.compile_profile(&test_data.profile_config).unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let start1 = std::time::Instant::now();
let result1 = rt.block_on(async {
scorer.calculate_score(&compiled_profile, &test_data.metrics, &test_data.document).await
}).unwrap();
let time1 = start1.elapsed();
let start2 = std::time::Instant::now();
let result2 = rt.block_on(async {
scorer.calculate_score(&compiled_profile, &test_data.metrics, &test_data.document).await
}).unwrap();
let time2 = start2.elapsed();
assert!((result1.final_score - result2.final_score).abs() < 0.01, "Results should be consistent");
assert!(time2.as_millis() <= time1.as_millis() + 50, "Caching should maintain performance");
}
#[test]
fn test_error_handling() {
let compiler = ProfileCompiler::new();
let mut invalid_profile = Phase3TestData::create_test_profile();
invalid_profile.metric_weights.clear();
let result = compiler.compile_profile(&invalid_profile);
match result {
Ok(compiled) => {
assert!(!compiled.metric_rules.is_empty(), "Should have default metric rules");
},
Err(e) => {
assert!(!e.to_string().is_empty(), "Error should have meaningful message");
}
}
}
#[test]
fn test_content_type_specific_scoring() {
let mut test_data = Phase3TestData::new();
let system = Phase3ScoringSystem::new();
test_data.profile_config.target_content_type = ContentType::Article;
let rt = tokio::runtime::Runtime::new().unwrap();
let article_result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
}).unwrap();
test_data.profile_config.target_content_type = ContentType::Product;
let product_result = rt.block_on(async {
system.complete_analysis(
&test_data.profile_config,
&test_data.metrics,
&test_data.document
).await
}).unwrap();
assert!(article_result.scoring_result.final_score >= 0.0);
assert!(product_result.scoring_result.final_score >= 0.0);
}
}