vex_core/
genome_experiment.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::evolution::Genome;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GenomeExperiment {
17 pub traits: Vec<f64>,
19 pub trait_names: Vec<String>,
21 pub fitness_scores: HashMap<String, f64>,
23 pub overall_fitness: f64,
25 pub task_summary: String,
27 pub successful: bool,
29 pub id: uuid::Uuid,
31 pub timestamp: chrono::DateTime<chrono::Utc>,
33}
34
35impl GenomeExperiment {
36 pub fn new(
57 genome: &Genome,
58 mut fitness_scores: HashMap<String, f64>,
59 overall: f64,
60 task: &str,
61 ) -> Self {
62 fitness_scores.retain(|k, v| {
64 !k.is_empty() && k.len() < 100 && v.is_finite() && *v >= 0.0 && *v <= 1.0
69 });
70
71 let overall_fitness = if overall.is_finite() && (0.0..=1.0).contains(&overall) {
73 overall
74 } else {
75 0.5 };
77
78 let sanitized_task: String = task
80 .chars()
81 .filter(|c| {
82 c.is_alphanumeric()
84 || c.is_whitespace() && *c == ' ' || ".,!?-_:;()[]{}".contains(*c)
86 })
87 .take(200)
88 .collect();
89
90 Self {
91 traits: genome.traits.clone(),
92 trait_names: genome.trait_names.clone(),
93 fitness_scores,
94 overall_fitness,
95 task_summary: sanitized_task,
96 successful: overall_fitness > 0.6,
97 id: uuid::Uuid::new_v4(),
98 timestamp: chrono::Utc::now(),
99 }
100 }
101
102 pub fn from_raw(
104 traits: Vec<f64>,
105 trait_names: Vec<String>,
106 overall_fitness: f64,
107 task_summary: &str,
108 ) -> Self {
109 Self {
110 traits,
111 trait_names,
112 fitness_scores: HashMap::new(),
113 overall_fitness,
114 task_summary: task_summary.to_string(),
115 successful: overall_fitness > 0.6,
116 id: uuid::Uuid::new_v4(),
117 timestamp: chrono::Utc::now(),
118 }
119 }
120
121 pub fn get_trait(&self, name: &str) -> Option<f64> {
123 self.trait_names
124 .iter()
125 .position(|n| n == name)
126 .and_then(|i| self.traits.get(i).copied())
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_experiment_creation() {
136 let genome = Genome::new("Test");
137 let mut scores = HashMap::new();
138 scores.insert("accuracy".to_string(), 0.9);
139 scores.insert("coherence".to_string(), 0.8);
140
141 let exp = GenomeExperiment::new(&genome, scores, 0.85, "Test task");
142
143 assert_eq!(exp.traits.len(), 5);
144 assert_eq!(exp.overall_fitness, 0.85);
145 assert!(exp.successful);
146 assert_eq!(exp.task_summary, "Test task");
147 }
148
149 #[test]
150 fn test_get_trait() {
151 let genome = Genome::new("Test");
152 let exp = GenomeExperiment::new(&genome, HashMap::new(), 0.5, "Task");
153
154 assert!(exp.get_trait("exploration").is_some());
155 assert_eq!(exp.get_trait("exploration"), Some(0.5));
156 assert!(exp.get_trait("nonexistent").is_none());
157 }
158
159 #[test]
160 fn test_success_threshold() {
161 let genome = Genome::new("Test");
162
163 let success = GenomeExperiment::new(&genome, HashMap::new(), 0.7, "Task");
164 assert!(success.successful);
165
166 let failure = GenomeExperiment::new(&genome, HashMap::new(), 0.5, "Task");
167 assert!(!failure.successful);
168 }
169
170 #[test]
173 fn test_task_injection_sanitized() {
174 let genome = Genome::new("test");
175 let malicious = "Task\x00\n\rINJECTED\x1b[31mRED";
176 let exp = GenomeExperiment::new(&genome, HashMap::new(), 0.5, malicious);
177
178 assert!(!exp.task_summary.contains('\x00'), "Null byte not removed");
179 assert!(!exp.task_summary.contains('\n'), "Newline not removed");
180 assert!(
181 !exp.task_summary.contains('\r'),
182 "Carriage return not removed"
183 );
184 assert!(
185 !exp.task_summary.contains('\x1b'),
186 "Escape sequence not removed"
187 );
188 }
189
190 #[test]
191 fn test_fitness_nan_validation() {
192 let genome = Genome::new("test");
193 let mut scores = HashMap::new();
194 scores.insert("nan_metric".to_string(), f64::NAN);
195 scores.insert("inf_metric".to_string(), f64::INFINITY);
196 scores.insert("valid_metric".to_string(), 0.8);
197 scores.insert("out_of_range".to_string(), 1.5);
198
199 let exp = GenomeExperiment::new(&genome, scores, f64::NAN, "task");
200
201 assert!(!exp.fitness_scores.contains_key("nan_metric"));
203 assert!(!exp.fitness_scores.contains_key("inf_metric"));
204 assert!(!exp.fitness_scores.contains_key("out_of_range"));
205
206 assert_eq!(exp.fitness_scores.get("valid_metric"), Some(&0.8));
208
209 assert_eq!(exp.overall_fitness, 0.5);
211 }
212
213 #[test]
214 fn test_fitness_key_length_limit() {
215 let genome = Genome::new("test");
216 let mut scores = HashMap::new();
217 scores.insert("A".repeat(200), 0.5); scores.insert("valid_key".to_string(), 0.8);
219 scores.insert("".to_string(), 0.9); let exp = GenomeExperiment::new(&genome, scores, 0.5, "task");
222
223 assert!(exp.fitness_scores.len() == 1);
225 assert_eq!(exp.fitness_scores.get("valid_key"), Some(&0.8));
226 }
227}