use anyhow::Result;
use camino::Utf8PathBuf;
use proptest::prelude::*;
use std::env;
use std::fs;
use std::thread;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use xchecker::cache::InsightCache;
use xchecker::logging::Logger;
use xchecker::packet::PacketBuilder;
#[test]
fn test_cache_initialization() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?.join("cache");
let builder = PacketBuilder::with_cache(cache_dir.clone())?;
assert!(builder.cache().is_some());
assert!(cache_dir.exists());
let cache = builder.cache().unwrap();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.invalidations, 0);
assert_eq!(stats.writes, 0);
Ok(())
}
#[test]
fn test_cache_hit_returns_cached_insights() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(
base_path.join("test.md"),
"# Test Document\nThis is test content for caching.",
)?;
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.misses > 0);
assert!(stats1.writes > 0);
assert!(packet1.content.contains("INSIGHTS:") || packet1.content.contains("CACHED INSIGHTS:"));
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let packet2 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(
stats2.hits > 0,
"Expected cache hits but got: hits={}, misses={}",
stats2.hits,
stats2.misses
);
assert!(packet2.content.contains("CACHED INSIGHTS:"));
assert!(packet2.content.contains("test.md"));
Ok(())
}
#[test]
fn test_cache_miss_generates_and_stores_insights() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(
base_path.join("design.md"),
"# Design Document\n## Architecture\nComponent-based design.",
)?;
let mut builder = PacketBuilder::with_cache(cache_dir.clone())?;
let packet = builder.build_packet(&base_path, "design", &context_dir, None)?;
let stats = builder.cache().unwrap().stats();
assert!(stats.misses > 0);
assert_eq!(stats.hits, 0);
assert!(stats.writes > 0);
assert!(packet.content.contains("INSIGHTS:"));
let cache_files: Vec<_> = fs::read_dir(&cache_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("json"))
.collect();
assert!(!cache_files.is_empty());
Ok(())
}
#[test]
fn test_cache_invalidation_on_file_change() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
let test_file = base_path.join("test.md");
fs::write(&test_file, "# Original Content")?;
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.misses > 0);
assert!(stats1.writes > 0);
thread::sleep(Duration::from_millis(100));
fs::write(&test_file, "# Modified Content\nNew information added.")?;
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let _packet2 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(stats2.invalidations > 0 || stats2.misses > 0);
Ok(())
}
#[test]
fn test_cache_performance_improvement() -> Result<()> {
const TOLERANCE: f64 = 2.0;
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
for i in 0..10 {
let content = format!(
"# Document {}\n{}\n",
i,
"## Section\nContent line\n".repeat(50)
);
fs::write(base_path.join(format!("doc{}.md", i)), content)?;
}
let start_no_cache = Instant::now();
let mut builder_no_cache = PacketBuilder::new()?;
let _packet_no_cache =
builder_no_cache.build_packet(&base_path, "requirements", &context_dir, None)?;
let duration_no_cache = start_no_cache.elapsed();
let start_first_cache = Instant::now();
let mut builder_first = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet_first =
builder_first.build_packet(&base_path, "requirements", &context_dir, None)?;
let duration_first_cache = start_first_cache.elapsed();
let start_second_cache = Instant::now();
let mut builder_second = PacketBuilder::with_cache(cache_dir)?;
let _packet_second =
builder_second.build_packet(&base_path, "requirements", &context_dir, None)?;
let duration_second_cache = start_second_cache.elapsed();
let stats = builder_second.cache().unwrap().stats();
assert!(stats.hits > 0);
println!(
"Performance: no_cache={:?}, first_cache={:?}, second_cache={:?}",
duration_no_cache, duration_first_cache, duration_second_cache
);
let hit_ms = duration_second_cache.as_secs_f64() * 1000.0;
let miss_ms = duration_first_cache.as_secs_f64() * 1000.0;
let ratio = if miss_ms > 0.0 { hit_ms / miss_ms } else { 0.0 };
assert!(
hit_ms <= miss_ms * TOLERANCE,
"Cache hit should be at most {}x cache miss time, got {:.2}x (hit={:.2}ms, miss={:.2}ms)",
TOLERANCE,
ratio,
hit_ms,
miss_ms
);
Ok(())
}
fn median(times: &mut [f64]) -> f64 {
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let len = times.len();
if len == 0 {
return 0.0;
}
if len.is_multiple_of(2) {
(times[len / 2 - 1] + times[len / 2]) / 2.0
} else {
times[len / 2]
}
}
#[test]
fn test_cache_performance_median_comparison() -> Result<()> {
const TOLERANCE: f64 = 2.0;
const NUM_RUNS: usize = 5;
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
for i in 0..10 {
let content = format!(
"# Document {}\n{}\n",
i,
"## Section\nContent line\n".repeat(50)
);
fs::write(base_path.join(format!("doc{}.md", i)), content)?;
}
let mut builder_init = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet_init = builder_init.build_packet(&base_path, "requirements", &context_dir, None)?;
let mut miss_times: Vec<f64> = Vec::with_capacity(NUM_RUNS);
for _ in 0..NUM_RUNS {
let miss_temp = TempDir::new()?;
let miss_cache_dir = Utf8PathBuf::try_from(miss_temp.path().to_path_buf())?;
let start = Instant::now();
let mut builder = PacketBuilder::with_cache(miss_cache_dir)?;
let _packet = builder.build_packet(&base_path, "requirements", &context_dir, None)?;
miss_times.push(start.elapsed().as_secs_f64() * 1000.0);
}
let mut hit_times: Vec<f64> = Vec::with_capacity(NUM_RUNS);
for _ in 0..NUM_RUNS {
let start = Instant::now();
let mut builder = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet = builder.build_packet(&base_path, "requirements", &context_dir, None)?;
hit_times.push(start.elapsed().as_secs_f64() * 1000.0);
let stats = builder.cache().unwrap().stats();
assert!(stats.hits > 0, "Expected cache hits but got none");
}
let miss_median = median(&mut miss_times);
let hit_median = median(&mut hit_times);
let ratio = if miss_median > 0.0 {
hit_median / miss_median
} else {
0.0
};
println!(
"Median performance: miss_median={:.2}ms, hit_median={:.2}ms, ratio={:.2}x",
miss_median, hit_median, ratio
);
println!(" Miss times: {:?}", miss_times);
println!(" Hit times: {:?}", hit_times);
assert!(
hit_median <= miss_median * TOLERANCE,
"Cache hit median ({:.2}ms) should be at most {}x cache miss median ({:.2}ms), got {:.2}x",
hit_median,
TOLERANCE,
miss_median,
ratio
);
Ok(())
}
#[test]
fn test_cache_hit_rate_repeated_runs() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
for i in 0..5 {
fs::write(
base_path.join(format!("file{}.md", i)),
format!("# File {}\nContent for file {}.", i, i),
)?;
}
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let mut total_hits = 0;
let mut total_misses = 0;
for _ in 0..5 {
let mut builder = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet = builder.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats = builder.cache().unwrap().stats();
total_hits += stats.hits;
total_misses += stats.misses;
}
let total_requests = total_hits + total_misses;
let hit_rate = if total_requests > 0 {
(total_hits as f64 / total_requests as f64) * 100.0
} else {
0.0
};
println!(
"Cache hit rate: {:.1}% ({} hits, {} misses)",
hit_rate, total_hits, total_misses
);
assert!(
hit_rate > 70.0,
"Cache hit rate should be >70%, got {:.1}%",
hit_rate
);
Ok(())
}
#[test]
fn test_cache_statistics_logging() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(base_path.join("test.md"), "# Test")?;
let logger = Logger::new(true);
let mut builder = PacketBuilder::with_cache(cache_dir)?;
let _packet = builder.build_packet(&base_path, "requirements", &context_dir, Some(&logger))?;
builder.log_cache_stats(&logger);
let stats = builder.cache().unwrap().stats();
assert!(stats.hits + stats.misses > 0);
Ok(())
}
#[test]
fn test_cache_with_different_phases() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(
base_path.join("doc.md"),
"# Document\nContent for testing phases.",
)?;
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.misses > 0);
assert!(stats1.writes > 0);
let mut builder2 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet2 = builder2.build_packet(&base_path, "design", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(stats2.misses > 0);
let mut builder3 = PacketBuilder::with_cache(cache_dir)?;
let _packet3 = builder3.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats3 = builder3.cache().unwrap().stats();
assert!(stats3.hits > 0);
Ok(())
}
#[test]
fn test_cache_with_mixed_priority_files() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(base_path.join("upstream.core.yaml"), "key: value")?;
fs::write(base_path.join("SPEC.md"), "# Specification")?;
fs::write(base_path.join("README.md"), "# Readme")?;
fs::write(base_path.join("config.toml"), "setting = true")?;
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.misses >= 4); assert!(stats1.writes >= 4);
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let _packet2 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(
stats2.hits >= 4,
"Expected at least 4 cache hits but got: hits={}, misses={}",
stats2.hits,
stats2.misses
);
Ok(())
}
#[test]
fn test_cache_persistence_across_instances() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(base_path.join("persistent.md"), "# Persistent Content")?;
{
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.writes > 0);
}
{
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let _packet2 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(
stats2.hits > 0,
"Expected cache hits from disk but got: hits={}, misses={}",
stats2.hits,
stats2.misses
);
}
Ok(())
}
#[test]
fn test_cache_with_empty_files() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(base_path.join("empty.md"), "")?;
let mut builder = PacketBuilder::with_cache(cache_dir)?;
let result = builder.build_packet(&base_path, "requirements", &context_dir, None);
assert!(result.is_ok());
let stats = builder.cache().unwrap().stats();
assert!(stats.hits + stats.misses > 0);
Ok(())
}
#[test]
fn test_cache_insight_generation_quality() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let cache = InsightCache::new(cache_dir)?;
let req_content = r#"
# Requirements Document
## Requirements
### Requirement 1
**User Story:** As a developer, I want to test, so that I can verify functionality.
#### Acceptance Criteria
1. WHEN I run tests THEN the system SHALL pass
2. WHEN errors occur THEN the system SHALL report them
"#;
let insights = cache.generate_insights(
req_content,
Utf8PathBuf::from("requirements.md").as_path(),
"requirements",
xchecker::types::Priority::High,
);
assert!(insights.len() >= 10, "Should have at least 10 insights");
assert!(insights.len() <= 25, "Should have at most 25 insights");
let insights_text = insights.join(" ");
assert!(
insights_text.contains("user")
|| insights_text.contains("User")
|| insights_text.contains("story"),
"Should mention user stories"
);
Ok(())
}
#[test]
fn test_cache_with_large_files() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
let large_content = "# Large Document\n".to_string() + &"Content line\n".repeat(1000);
fs::write(base_path.join("large.md"), &large_content)?;
let mut builder = PacketBuilder::with_cache(cache_dir.clone())?;
let _packet1 = builder.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder.cache().unwrap().stats();
assert!(stats1.writes > 0);
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let _packet2 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(stats2.hits > 0);
Ok(())
}
#[test]
fn test_cache_memory_disk_consistency() -> Result<()> {
let temp_dir = TempDir::new()?;
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
fs::write(base_path.join("test.md"), "# Test Content")?;
let mut builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
let packet1 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let packet2 = builder1.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats1 = builder1.cache().unwrap().stats();
assert!(stats1.hits > 0);
let mut builder2 = PacketBuilder::with_cache(cache_dir)?;
let packet3 = builder2.build_packet(&base_path, "requirements", &context_dir, None)?;
let stats2 = builder2.cache().unwrap().stats();
assert!(stats2.hits > 0);
assert!(packet1.content.contains("INSIGHTS:") || packet1.content.contains("CACHED INSIGHTS:"));
assert!(packet2.content.contains("CACHED INSIGHTS:"));
assert!(packet3.content.contains("CACHED INSIGHTS:"));
Ok(())
}
#[test]
fn test_cache_builder_methods() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = Utf8PathBuf::try_from(temp_dir.path().to_path_buf())?;
let builder1 = PacketBuilder::with_cache(cache_dir.clone())?;
assert!(builder1.cache().is_some());
let builder2 = PacketBuilder::with_limits_and_cache(32768, 600, cache_dir.clone())?;
assert!(builder2.cache().is_some());
let mut builder3 = PacketBuilder::with_cache(cache_dir.clone())?;
assert!(builder3.cache_mut().is_some());
let mut builder4 = PacketBuilder::new()?;
assert!(builder4.cache().is_none());
let new_cache = InsightCache::new(cache_dir.clone())?;
builder4.set_cache(new_cache);
assert!(builder4.cache().is_some());
builder4.remove_cache();
assert!(builder4.cache().is_none());
Ok(())
}
const DEFAULT_PROPTEST_CASES: u32 = 64;
const DEFAULT_MAX_SHRINK_ITERS: u32 = 1000;
fn proptest_config(max_cases: Option<u32>) -> ProptestConfig {
let env_cases = env::var("PROPTEST_CASES")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(DEFAULT_PROPTEST_CASES);
let env_shrink_iters = env::var("PROPTEST_MAX_SHRINK_ITERS")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(DEFAULT_MAX_SHRINK_ITERS);
let cases = match max_cases {
Some(max) => env_cases.min(max),
None => env_cases,
};
ProptestConfig {
cases,
max_shrink_iters: env_shrink_iters,
max_shrink_time: 30000, ..ProptestConfig::default()
}
}
#[test]
fn prop_cache_hit_not_slower_than_miss() {
const TOLERANCE: f64 = 2.0;
const NUM_RUNS: usize = 5;
let config = proptest_config(Some(10));
proptest!(config, |(
// Generate varying file counts (5-10 files)
file_count in 5usize..=10,
content_lines in 50usize..=100
)| {
let temp_dir = TempDir::new().unwrap();
let base_path = Utf8PathBuf::try_from(temp_dir.path().to_path_buf()).unwrap();
let context_dir = base_path.join("context");
let cache_dir = base_path.join("cache");
for i in 0..file_count {
let content = format!(
"# Document {}\n{}\n",
i,
"## Section\nContent line for testing cache performance.\n".repeat(content_lines)
);
fs::write(base_path.join(format!("doc{}.md", i)), content).unwrap();
}
let mut builder_init = PacketBuilder::with_cache(cache_dir.clone()).unwrap();
let _packet_init = builder_init
.build_packet(&base_path, "requirements", &context_dir, None)
.unwrap();
let mut miss_times: Vec<f64> = Vec::with_capacity(NUM_RUNS);
for _ in 0..NUM_RUNS {
let miss_temp = TempDir::new().unwrap();
let miss_cache_dir = Utf8PathBuf::try_from(miss_temp.path().to_path_buf()).unwrap();
let start = Instant::now();
let mut builder = PacketBuilder::with_cache(miss_cache_dir).unwrap();
let _packet = builder
.build_packet(&base_path, "requirements", &context_dir, None)
.unwrap();
miss_times.push(start.elapsed().as_secs_f64() * 1000.0);
}
let mut hit_times: Vec<f64> = Vec::with_capacity(NUM_RUNS);
for _ in 0..NUM_RUNS {
let start = Instant::now();
let mut builder = PacketBuilder::with_cache(cache_dir.clone()).unwrap();
let _packet = builder
.build_packet(&base_path, "requirements", &context_dir, None)
.unwrap();
hit_times.push(start.elapsed().as_secs_f64() * 1000.0);
let stats = builder.cache().unwrap().stats();
prop_assert!(stats.hits > 0, "Expected cache hits but got none");
}
let miss_median = median(&mut miss_times);
let hit_median = median(&mut hit_times);
let ratio = if miss_median > 0.0 {
hit_median / miss_median
} else {
0.0
};
const MIN_DURATION_MS: f64 = 10.0;
if miss_median >= MIN_DURATION_MS && hit_median >= MIN_DURATION_MS {
prop_assert!(
hit_median <= miss_median * TOLERANCE,
"Cache hit median ({:.2}ms) should be at most {}x cache miss median ({:.2}ms), got {:.2}x\n\
File count: {}, Content lines: {}\n\
Miss times: {:?}\n\
Hit times: {:?}",
hit_median,
TOLERANCE,
miss_median,
ratio,
file_count,
content_lines,
miss_times,
hit_times
);
}
});
}