do-memory-core 0.1.31

Core episodic learning system for AI agents with pattern extraction, reward scoring, and dual storage backend
Documentation
//! Property-based tests for serialization round-trips and calculator invariants
//!
//! Implements ADR-042 ACT-030 Phase 2: Property tests for serialization and calculations.

#![allow(
    clippy::cast_precision_loss,
    clippy::cast_sign_loss,
    clippy::cast_possible_truncation,
    clippy::field_reassign_with_default
)]

use chrono::{Duration, Utc};
use do_memory_core::episode::{Episode, ExecutionStep};
use do_memory_core::pattern::PatternEffectiveness;
use do_memory_core::reward::RewardCalculator;
use do_memory_core::types::{
    ComplexityLevel, ExecutionResult, OutcomeStats, TaskContext, TaskOutcome, TaskType,
};
use proptest::prelude::*;

// ============================================================================
// Custom Strategies
// ============================================================================

fn task_type_strategy() -> impl Strategy<Value = TaskType> {
    prop_oneof![
        Just(TaskType::CodeGeneration),
        Just(TaskType::Debugging),
        Just(TaskType::Refactoring),
        Just(TaskType::Testing),
        Just(TaskType::Analysis),
        Just(TaskType::Documentation),
        Just(TaskType::Other),
    ]
}

fn complexity_strategy() -> impl Strategy<Value = ComplexityLevel> {
    prop_oneof![
        Just(ComplexityLevel::Simple),
        Just(ComplexityLevel::Moderate),
        Just(ComplexityLevel::Complex),
    ]
}

// ============================================================================
// Serialization Round-trip Tests
// ============================================================================

proptest! {
    #[test]
    fn task_type_postcard_roundtrip(task_type in task_type_strategy()) {
        let encoded = postcard::to_allocvec(&task_type).expect("serialize");
        let decoded: TaskType = postcard::from_bytes(&encoded).expect("deserialize");
        prop_assert_eq!(task_type, decoded);
    }
}

proptest! {
    #[test]
    fn complexity_level_postcard_roundtrip(level in complexity_strategy()) {
        let encoded = postcard::to_allocvec(&level).expect("serialize");
        let decoded: ComplexityLevel = postcard::from_bytes(&encoded).expect("deserialize");
        prop_assert_eq!(level, decoded);
    }
}

// ============================================================================
// Reward Calculator Property Tests
// ============================================================================

proptest! {
    #[test]
    fn reward_calculator_efficiency_bounds(num_steps in 0usize..100usize, duration_secs in 0i64..3600i64) {
        let calculator = RewardCalculator::new();
        let mut episode = Episode::new("Test task".to_string(), TaskContext::default(), TaskType::CodeGeneration);

        for i in 0..num_steps {
            let step = ExecutionStep::new(i + 1, "test_tool".to_string(), "Test action".to_string());
            episode.add_step(step);
        }

        episode.complete(TaskOutcome::Success { verdict: "Done".to_string(), artifacts: vec![] });
        episode.start_time = Utc::now() - Duration::seconds(duration_secs);
        episode.end_time = Some(Utc::now());

        let score = calculator.calculate(&episode);

        prop_assert!(score.efficiency >= 0.5, "Efficiency below minimum 0.5");
        prop_assert!(score.efficiency <= 1.5, "Efficiency above maximum 1.5");
    }
}

proptest! {
    #[test]
    fn reward_calculator_base_bounds(outcome_success in proptest::bool::ANY) {
        let calculator = RewardCalculator::new();
        let mut episode = Episode::new("Test task".to_string(), TaskContext::default(), TaskType::CodeGeneration);

        let outcome = if outcome_success {
            TaskOutcome::Success { verdict: "Done".to_string(), artifacts: vec![] }
        } else {
            TaskOutcome::Failure { reason: "Failed".to_string(), error_details: None }
        };
        episode.complete(outcome);

        let score = calculator.calculate(&episode);

        prop_assert!(score.base >= 0.0, "Base below minimum 0.0");
        prop_assert!(score.base <= 1.0, "Base above maximum 1.0");
    }
}

proptest! {
    #[test]
    fn reward_calculator_complexity_bonus_bounds(complexity in complexity_strategy()) {
        let calculator = RewardCalculator::new();
        let mut context = TaskContext::default();
        context.complexity = complexity;

        let mut episode = Episode::new("Test task".to_string(), context, TaskType::CodeGeneration);
        episode.complete(TaskOutcome::Success { verdict: "Done".to_string(), artifacts: vec![] });

        let score = calculator.calculate(&episode);

        prop_assert!(score.complexity_bonus >= 1.0);
        prop_assert!(score.complexity_bonus <= 1.2);
    }
}

// ============================================================================
// Pattern Effectiveness Property Tests
// ============================================================================

proptest! {
    #[test]
    fn pattern_effectiveness_usage_rate_bounds(times_retrieved in 0usize..100usize, times_applied in 0usize..100usize) {
        let mut effectiveness = PatternEffectiveness::default();
        effectiveness.times_retrieved = times_retrieved;
        effectiveness.times_applied = times_applied.min(times_retrieved);

        let usage_rate = effectiveness.usage_rate();

        prop_assert!(usage_rate >= 0.0);
        prop_assert!(usage_rate <= 1.0);
    }
}

proptest! {
    #[test]
    fn pattern_effectiveness_success_rate_bounds(success_when_applied in 0usize..50usize, failure_when_applied in 0usize..50usize) {
        let mut effectiveness = PatternEffectiveness::default();
        effectiveness.success_when_applied = success_when_applied;
        effectiveness.failure_when_applied = failure_when_applied;
        effectiveness.times_applied = success_when_applied + failure_when_applied;

        let success_rate = effectiveness.application_success_rate();

        prop_assert!(success_rate >= 0.0);
        prop_assert!(success_rate <= 1.0);
    }
}

proptest! {
    #[test]
    fn pattern_effectiveness_score_non_negative(times_applied in 0usize..100usize, success_rate in 0.0f32..1.0f32, reward_delta in -1.0f32..1.0f32) {
        let mut effectiveness = PatternEffectiveness::default();
        effectiveness.times_applied = times_applied;
        effectiveness.success_when_applied = (times_applied as f32 * success_rate) as usize;
        effectiveness.failure_when_applied = times_applied - effectiveness.success_when_applied;
        effectiveness.avg_reward_delta = reward_delta;

        let score = effectiveness.effectiveness_score();

        prop_assert!(score >= 0.0, "Effectiveness score is negative");
    }
}

// ============================================================================
// Outcome Stats Property Tests
// ============================================================================

proptest! {
    #[test]
    fn outcome_stats_success_rate_bounds(success_count in 0usize..100usize, failure_count in 0usize..100usize) {
        let stats = OutcomeStats {
            success_count,
            failure_count,
            total_count: success_count + failure_count,
            avg_duration_secs: 0.0,
        };

        let success_rate = stats.success_rate();

        prop_assert!(success_rate >= 0.0);
        prop_assert!(success_rate <= 1.0);
    }
}

#[test]
fn outcome_stats_success_rate_zero_total() {
    let stats = OutcomeStats {
        success_count: 0,
        failure_count: 0,
        total_count: 0,
        avg_duration_secs: 0.0,
    };
    let success_rate = stats.success_rate();
    assert!((success_rate - 0.0_f32).abs() < f32::EPSILON);
}

// ============================================================================
// Episode Property Tests
// ============================================================================

proptest! {
    #[test]
    fn episode_step_count_consistency(step_results in proptest::collection::vec(proptest::bool::ANY, 0..50usize)) {
        let mut episode = Episode::new("Test".to_string(), TaskContext::default(), TaskType::Testing);

        let expected_successes = step_results.iter().filter(|&&r| r).count();
        let expected_failures = step_results.iter().filter(|&&r| !r).count();

        for (i, &is_success) in step_results.iter().enumerate() {
            let mut step = ExecutionStep::new(i + 1, "tool".to_string(), "action".to_string());
            step.result = if is_success {
                Some(ExecutionResult::Success { output: "ok".to_string() })
            } else {
                Some(ExecutionResult::Error { message: "err".to_string() })
            };
            episode.add_step(step);
        }

        prop_assert_eq!(episode.successful_steps_count(), expected_successes);
        prop_assert_eq!(episode.failed_steps_count(), expected_failures);
    }
}

proptest! {
    #[test]
    fn episode_completion_state(num_steps in 0usize..10usize, should_complete in proptest::bool::ANY) {
        let mut episode = Episode::new("Test".to_string(), TaskContext::default(), TaskType::CodeGeneration);

        for i in 0..num_steps {
            let step = ExecutionStep::new(i + 1, "tool".to_string(), "action".to_string());
            episode.add_step(step);
        }

        if should_complete {
            episode.complete(TaskOutcome::Success { verdict: "Done".to_string(), artifacts: vec![] });
        }

        prop_assert_eq!(episode.is_complete(), should_complete);
    }
}