use do_memory_core::*;
use proptest::prelude::*;
use std::result::Result as StdResult;
use chrono::Utc;
proptest! {
#[test]
fn task_context_json_roundtrip(
language in proptest::option::of("[a-z]{2,10}"),
framework in proptest::option::of("[a-z]{2,15}"),
domain in "[a-z]{3,20}",
tags in proptest::collection::vec("[a-z]{2,15}", 0..10)
) {
let context = TaskContext {
language,
framework,
complexity: ComplexityLevel::Moderate,
domain,
tags,
};
let json = serde_json::to_string(&context).expect("serialize to JSON");
let deserialized: TaskContext = serde_json::from_str(&json).expect("deserialize from JSON");
assert_eq!(context, deserialized);
}
#[test]
fn reward_score_json_roundtrip(
total in 0.0f32..2.0f32,
base in 0.0f32..1.0f32,
efficiency in 0.5f32..1.5f32,
complexity_bonus in 1.0f32..1.3f32,
quality_multiplier in 0.8f32..1.2f32,
learning_bonus in 0.0f32..0.5f32
) {
let score = RewardScore {
total,
base,
efficiency,
complexity_bonus,
quality_multiplier,
learning_bonus,
};
let json = serde_json::to_string(&score).expect("serialize to JSON");
let deserialized: RewardScore = serde_json::from_str(&json).expect("deserialize from JSON");
assert!((score.total - deserialized.total).abs() < 0.001);
assert!((score.base - deserialized.base).abs() < 0.001);
assert!((score.efficiency - deserialized.efficiency).abs() < 0.001);
}
#[test]
fn outcome_stats_success_rate_invariant(
success_count in 0usize..1000usize,
failure_count in 0usize..1000usize,
avg_duration_secs in 0.0f32..3600.0f32
) {
let stats = OutcomeStats {
success_count,
failure_count,
total_count: success_count + failure_count,
avg_duration_secs,
};
let success_rate = stats.success_rate();
prop_assert!((0.0..=1.0).contains(&success_rate));
if stats.total_count > 0 {
#[allow(clippy::cast_precision_loss)]
let expected_rate = success_count as f32 / stats.total_count as f32;
prop_assert!((success_rate - expected_rate).abs() < 0.0001);
} else {
prop_assert!((success_rate - 0.0_f32).abs() < f32::EPSILON);
}
}
#[test]
fn episode_tag_validation_invariants(
tag in "[a-zA-Z0-9_-]{1,100}"
) {
let mut episode = Episode::new(
"Test task".to_string(),
TaskContext::default(),
TaskType::CodeGeneration
);
if tag.len() >= 2 && tag.len() <= 100 {
let result = episode.add_tag(tag.clone());
prop_assert!(result.is_ok());
let normalized_tag = tag.trim().to_lowercase();
prop_assert!(episode.has_tag(&normalized_tag));
}
}
#[test]
fn tag_normalization_consistency(
tag in "[a-zA-Z0-9_-]{2,50}"
) {
let mut episode = Episode::new(
"Test task".to_string(),
TaskContext::default(),
TaskType::CodeGeneration
);
episode.add_tag(tag.clone()).unwrap();
let upper = tag.to_uppercase();
let mixed: String = tag.chars()
.enumerate()
.map(|(i, c)| if i % 2 == 0 { c.to_ascii_uppercase() } else { c })
.collect();
prop_assert!(episode.has_tag(&upper));
prop_assert!(episode.has_tag(&mixed));
prop_assert!(episode.has_tag(&tag.to_lowercase()));
}
#[test]
fn episode_json_roundtrip(
task_description in "[a-zA-Z0-9 ]{1,100}",
domain in "[a-z]{3,15}",
has_outcome in proptest::bool::ANY
) {
let mut episode = Episode::new(
task_description.clone(),
TaskContext {
language: Some("rust".to_string()),
framework: None,
complexity: ComplexityLevel::Moderate,
domain: domain.clone(),
tags: vec!["test".to_string()],
},
TaskType::CodeGeneration,
);
let step = ExecutionStep::new(
1,
"test_tool".to_string(),
"Test action".to_string()
);
episode.add_step(step);
if has_outcome {
episode.complete(TaskOutcome::Success {
verdict: "Test completed".to_string(),
artifacts: vec!["file.rs".to_string()],
});
}
let episode_id = episode.episode_id;
let steps_len = episode.steps.len();
let is_complete = episode.is_complete();
let json = serde_json::to_string(&episode).expect("serialize episode");
let deserialized: Episode = serde_json::from_str(&json).expect("deserialize episode");
let deserialized_id = deserialized.episode_id;
let deserialized_steps_len = deserialized.steps.len();
let deserialized_is_complete = deserialized.is_complete();
let deserialized_task_desc = deserialized.task_description.clone();
let deserialized_domain = deserialized.context.domain.clone();
prop_assert_eq!(episode_id, deserialized_id);
prop_assert_eq!(task_description, deserialized_task_desc);
prop_assert_eq!(domain, deserialized_domain);
prop_assert_eq!(steps_len, deserialized_steps_len);
prop_assert_eq!(is_complete, deserialized_is_complete);
}
}
proptest! {
#[test]
fn episode_state_transition_invariants(
num_steps in 0usize..50usize
) {
let mut episode = Episode::new(
"Test task".to_string(),
TaskContext::default(),
TaskType::CodeGeneration,
);
prop_assert!(!episode.is_complete());
prop_assert_eq!(episode.steps.len(), 0);
prop_assert!(episode.outcome.is_none());
prop_assert!(episode.end_time.is_none());
for i in 0..num_steps {
let step = ExecutionStep::new(
i + 1,
format!("tool_{i}"),
format!("Action {i}")
);
episode.add_step(step);
}
prop_assert!(!episode.is_complete());
prop_assert_eq!(episode.steps.len(), num_steps);
episode.complete(TaskOutcome::Success {
verdict: "Done".to_string(),
artifacts: vec![],
});
prop_assert!(episode.is_complete());
prop_assert!(episode.outcome.is_some());
prop_assert!(episode.end_time.is_some());
prop_assert!(episode.duration().is_some());
}
#[test]
fn step_success_counting_invariants(
results in proptest::collection::vec(
proptest::bool::ANY,
1..100usize
)
) {
let mut episode = Episode::new(
"Test task".to_string(),
TaskContext::default(),
TaskType::Testing,
);
let expected_successes = results.iter().filter(|&&r| r).count();
let expected_failures = results.iter().filter(|&&r| !r).count();
for (i, is_success) in results.iter().enumerate() {
let mut step = ExecutionStep::new(
i + 1,
"test_tool".to_string(),
"Test".to_string()
);
step.result = if *is_success {
Some(ExecutionResult::Success {
output: "OK".to_string()
})
} else {
Some(ExecutionResult::Error {
message: "Failed".to_string()
})
};
episode.add_step(step);
}
prop_assert_eq!(episode.successful_steps_count(), expected_successes);
prop_assert_eq!(episode.failed_steps_count(), expected_failures);
prop_assert_eq!(
episode.successful_steps_count() + episode.failed_steps_count(),
results.len()
);
}
#[test]
fn execution_result_invariants(
output in "[a-zA-Z0-9 ]{0,100}",
error_msg in "[a-zA-Z0-9 ]{0,100}"
) {
let success = ExecutionResult::Success {
output: output.clone()
};
let error = ExecutionResult::Error {
message: error_msg.clone()
};
let timeout = ExecutionResult::Timeout;
prop_assert!(success.is_success());
prop_assert!(!error.is_success());
prop_assert!(!timeout.is_success());
}
#[test]
fn pattern_effectiveness_invariants(
retrieved in 0usize..100usize,
_applied in 0usize..100usize,
successes in 0usize..100usize,
failures in 0usize..100usize
) {
let mut effectiveness = PatternEffectiveness::default();
for _ in 0..retrieved {
effectiveness.record_retrieval();
}
for _ in 0..successes {
effectiveness.record_application(true, 0.1);
}
for _ in 0..failures {
effectiveness.record_application(false, -0.1);
}
prop_assert_eq!(
effectiveness.times_applied,
successes + failures
);
let expected_usage_rate = if retrieved == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
{ (successes + failures) as f32 / retrieved as f32 }
};
prop_assert!(
(effectiveness.usage_rate() - expected_usage_rate).abs() < 0.0001
);
if effectiveness.times_applied > 0 {
#[allow(clippy::cast_precision_loss)]
let expected_success_rate = successes as f32 / effectiveness.times_applied as f32;
prop_assert!(
(effectiveness.application_success_rate() - expected_success_rate).abs() < 0.0001
);
} else {
prop_assert!((effectiveness.application_success_rate() - 0.5_f32).abs() < f32::EPSILON);
}
}
}
proptest! {
#[test]
fn task_outcome_json_roundtrip(
verdict in "[a-zA-Z0-9 ]{1,100}",
reason in "[a-zA-Z0-9 ]{1,100}"
) {
let outcomes = vec![
TaskOutcome::Success {
verdict: verdict.clone(),
artifacts: vec!["file1.rs".to_string()],
},
TaskOutcome::PartialSuccess {
verdict: verdict.clone(),
completed: vec!["item1".to_string()],
failed: vec!["item2".to_string()],
},
TaskOutcome::Failure {
reason: reason.clone(),
error_details: Some("Detailed error".to_string()),
},
];
for outcome in outcomes {
let json = serde_json::to_string(&outcome).expect("serialize outcome");
let deserialized: TaskOutcome = serde_json::from_str(&json).expect("deserialize outcome");
prop_assert_eq!(
std::mem::discriminant(&outcome),
std::mem::discriminant(&deserialized)
);
}
}
#[test]
fn task_type_string_roundtrip(
task_type in prop::sample::select(vec![
TaskType::CodeGeneration,
TaskType::Debugging,
TaskType::Refactoring,
TaskType::Testing,
TaskType::Analysis,
TaskType::Documentation,
TaskType::Other,
])
) {
let string_repr = task_type.to_string();
let parsed: TaskType = string_repr.parse().expect("parse task type");
prop_assert_eq!(task_type, parsed);
}
#[test]
fn complexity_level_roundtrip(
level in prop::sample::select(vec![
ComplexityLevel::Simple,
ComplexityLevel::Moderate,
ComplexityLevel::Complex,
])
) {
let json = serde_json::to_string(&level).expect("serialize complexity");
let deserialized: ComplexityLevel = serde_json::from_str(&json).expect("deserialize complexity");
prop_assert_eq!(level, deserialized);
}
}
proptest! {
#[test]
fn episode_edge_cases(
empty_desc in "",
long_desc in "[a-z]{200,300}"
) {
let episode1 = Episode::new(
empty_desc.clone(),
TaskContext::default(),
TaskType::Other,
);
let _episode2 = Episode::new(
long_desc.clone(),
TaskContext::default(),
TaskType::CodeGeneration,
);
prop_assert!(!episode1.is_complete());
let episode2 = Episode::new(
long_desc.clone(),
TaskContext::default(),
TaskType::CodeGeneration,
);
prop_assert_eq!(episode2.task_description.len(), long_desc.len());
}
#[test]
fn reflection_serialization_roundtrip(
successes in proptest::collection::vec("[a-zA-Z0-9 ]{1,50}", 0..20),
improvements in proptest::collection::vec("[a-zA-Z0-9 ]{1,50}", 0..20),
insights in proptest::collection::vec("[a-zA-Z0-9 ]{1,50}", 0..20)
) {
let reflection = Reflection {
successes,
improvements,
insights,
generated_at: Utc::now(),
};
let json = serde_json::to_string(&reflection).expect("serialize reflection");
let deserialized: Reflection = serde_json::from_str(&json).expect("deserialize reflection");
prop_assert_eq!(reflection.successes.len(), deserialized.successes.len());
prop_assert_eq!(reflection.improvements.len(), deserialized.improvements.len());
prop_assert_eq!(reflection.insights.len(), deserialized.insights.len());
}
#[test]
fn evidence_serialization_roundtrip(
sample_size in 1usize..100usize,
success_rate in 0.0f32..1.0f32
) {
use uuid::Uuid;
let evidence = Evidence {
episode_ids: (0..sample_size).map(|_| Uuid::new_v4()).collect(),
success_rate,
sample_size,
};
let json = serde_json::to_string(&evidence).expect("serialize evidence");
let deserialized: Evidence = serde_json::from_str(&json).expect("deserialize evidence");
prop_assert_eq!(evidence.episode_ids.len(), deserialized.episode_ids.len());
prop_assert!((evidence.success_rate - deserialized.success_rate).abs() < 0.0001);
}
}
proptest! {
#[test]
fn serialization_determinism(
task_description in "[a-zA-Z0-9 ]{1,50}",
domain in "[a-z]{3,15}"
) {
let episode = Episode::new(
task_description.clone(),
TaskContext {
language: Some("rust".to_string()),
framework: None,
complexity: ComplexityLevel::Moderate,
domain: domain.clone(),
tags: vec!["test".to_string()],
},
TaskType::CodeGeneration,
);
let json1 = serde_json::to_string(&episode).expect("serialize 1");
let json2 = serde_json::to_string(&episode).expect("serialize 2");
prop_assert_eq!(json1.clone(), json2.clone());
let de1: Episode = serde_json::from_str(&json1).expect("deserialize 1");
let de2: Episode = serde_json::from_str(&json2).expect("deserialize 2");
prop_assert_eq!(de1, de2);
}
#[test]
fn tag_operations_deterministic(
tags in proptest::collection::hash_set("[a-z]{2,20}", 1..50)
) {
let mut episode1 = Episode::new(
"Test".to_string(),
TaskContext::default(),
TaskType::Analysis,
);
let mut episode2 = Episode::new(
"Test".to_string(),
TaskContext::default(),
TaskType::Analysis,
);
let tag_vec: Vec<_> = tags.iter().cloned().collect();
for tag in &tag_vec {
episode1.add_tag(tag.clone()).unwrap();
episode2.add_tag(tag.clone()).unwrap();
}
prop_assert_eq!(episode1.get_tags(), episode2.get_tags());
for tag in &tag_vec {
prop_assert_eq!(
episode1.has_tag(tag),
episode2.has_tag(tag)
);
}
}
}
#[cfg(test)]
mod postcard_tests {
use super::*;
proptest! {
#[test]
fn task_context_postcard_roundtrip(
language in proptest::option::of("[a-z]{2,10}"),
framework in proptest::option::of("[a-z]{2,15}"),
domain in "[a-z]{3,20}",
tags in proptest::collection::vec("[a-z]{2,15}", 0..10)
) {
let context = TaskContext {
language,
framework,
complexity: ComplexityLevel::Moderate,
domain,
tags,
};
let serialized = postcard::to_allocvec(&context).expect("postcard serialize");
let deserialized: TaskContext = postcard::from_bytes(&serialized).expect("postcard deserialize");
assert_eq!(context, deserialized);
}
#[test]
fn reward_score_postcard_roundtrip(
total in 0.0f32..2.0f32,
base in 0.0f32..1.0f32,
efficiency in 0.5f32..1.5f32,
) {
let score = RewardScore {
total,
base,
efficiency,
complexity_bonus: 1.0,
quality_multiplier: 1.0,
learning_bonus: 0.0,
};
let serialized = postcard::to_allocvec(&score).expect("postcard serialize");
let deserialized: RewardScore = postcard::from_bytes(&serialized).expect("postcard deserialize");
assert!((score.total - deserialized.total).abs() < 0.001);
}
#[test]
fn episode_postcard_size_check(
task_description in "[a-zA-Z0-9 ]{1,200}",
num_steps in 0usize..20usize
) {
let mut episode = Episode::new(
task_description.clone(),
TaskContext::default(),
TaskType::CodeGeneration,
);
for i in 0..num_steps {
let step = ExecutionStep::new(
i + 1,
format!("tool_{}", i % 10),
format!("Action {i}")
);
episode.add_step(step);
}
let serialized = postcard::to_allocvec(&episode);
if let Ok(bytes) = serialized {
let deserialized: StdResult<Episode, _> = postcard::from_bytes(&bytes);
if let Ok(de) = deserialized {
prop_assert_eq!(episode.steps.len(), de.steps.len());
}
}
}
}
}
fn configure_proptest() -> ProptestConfig {
ProptestConfig {
cases: 100, max_shrink_iters: 50, ..ProptestConfig::default()
}
}
proptest! {
#![proptest_config(configure_proptest())]
#[test]
fn comprehensive_episode_lifecycle(
task_type in prop::sample::select(vec![
TaskType::CodeGeneration,
TaskType::Testing,
TaskType::Debugging,
]),
complexity in prop::sample::select(vec![
ComplexityLevel::Simple,
ComplexityLevel::Moderate,
ComplexityLevel::Complex,
]),
num_steps in 0usize..100usize,
) {
let context = TaskContext {
language: Some("rust".to_string()),
framework: Some("tokio".to_string()),
complexity,
domain: "test-domain".to_string(),
tags: vec!["test".to_string()],
};
let mut episode = Episode::new(
"Comprehensive test".to_string(),
context,
task_type,
);
for i in 0..num_steps {
let result = if i % 3 == 0 {
ExecutionResult::Error { message: "Error".to_string() }
} else {
ExecutionResult::Success { output: "OK".to_string() }
};
let mut step = ExecutionStep::new(
i + 1,
format!("step_{i}"),
"Action".to_string()
);
step.result = Some(result);
episode.add_step(step);
}
episode.complete(TaskOutcome::Success {
verdict: "Completed".to_string(),
artifacts: vec![],
});
prop_assert!(episode.is_complete());
prop_assert_eq!(episode.steps.len(), num_steps);
prop_assert!(episode.duration().is_some());
let json = serde_json::to_string(&episode).expect("serialize");
let deserialized: Episode = serde_json::from_str(&json).expect("deserialize");
prop_assert_eq!(episode.steps.len(), deserialized.steps.len());
}
}