1use std::collections::HashMap;
7use crate::types::SignalTag;
8
9#[derive(Debug)]
11pub struct GeneSelectionResult {
12 pub action: String,
14 pub gene_id: Option<String>,
15 pub gene: Option<serde_json::Value>,
16 pub strategy: Option<Vec<String>>,
17 pub confidence: f64,
18 pub coverage_score: Option<f64>,
19 pub alternatives: Vec<Alternative>,
20 pub reason: String,
21 pub from_cache: bool,
22}
23
24#[derive(Debug)]
26pub struct Alternative {
27 pub gene_id: String,
28 pub confidence: f64,
29 pub title: Option<String>,
30}
31
32pub struct EvolutionCache {
44 genes: HashMap<String, serde_json::Value>,
45 edges: HashMap<String, Vec<serde_json::Value>>,
46 global_prior: HashMap<String, (f64, f64)>, cursor: u64,
48}
49
50impl EvolutionCache {
51 pub fn new() -> Self {
53 Self {
54 genes: HashMap::new(),
55 edges: HashMap::new(),
56 global_prior: HashMap::new(),
57 cursor: 0,
58 }
59 }
60
61 pub fn gene_count(&self) -> usize {
63 self.genes.len()
64 }
65
66 pub fn cursor(&self) -> u64 {
68 self.cursor
69 }
70
71 pub fn load_snapshot(&mut self, snapshot: &serde_json::Value) {
73 self.genes.clear();
74 self.edges.clear();
75 self.global_prior.clear();
76
77 if let Some(genes) = snapshot.get("genes").and_then(|v| v.as_array()) {
79 for gene in genes {
80 let id = gene.get("id").or_else(|| gene.get("gene_id"))
81 .and_then(|v| v.as_str());
82 if let Some(id) = id {
83 self.genes.insert(id.to_string(), gene.clone());
84 }
85 }
86 }
87
88 if let Some(edges) = snapshot.get("edges").and_then(|v| v.as_array()) {
90 for edge in edges {
91 let key = edge.get("signal_key").or_else(|| edge.get("signalKey"))
92 .and_then(|v| v.as_str());
93 if let Some(key) = key {
94 self.edges.entry(key.to_string()).or_default().push(edge.clone());
95 }
96 }
97 }
98
99 let gp = snapshot.get("globalPrior").or_else(|| snapshot.get("global_prior"));
101 if let Some(gp) = gp.and_then(|v| v.as_object()) {
102 for (key, val) in gp {
103 if let Some(obj) = val.as_object() {
104 let alpha = obj.get("alpha").and_then(|v| v.as_f64()).unwrap_or(1.0);
105 let beta = obj.get("beta").and_then(|v| v.as_f64()).unwrap_or(1.0);
106 self.global_prior.insert(key.clone(), (alpha, beta));
107 } else if let Some(f) = val.as_f64() {
108 self.global_prior.insert(key.clone(), (f, 1.0));
109 }
110 }
111 }
112
113 if let Some(cur) = snapshot.get("cursor").and_then(|v| v.as_u64()) {
114 self.cursor = cur;
115 }
116 }
117
118 pub fn apply_delta(&mut self, delta: &serde_json::Value) {
120 let pulled = delta.get("pulled").unwrap_or(delta);
121
122 if let Some(genes) = pulled.get("genes").and_then(|v| v.as_array()) {
124 for gene in genes {
125 let id = gene.get("id").or_else(|| gene.get("gene_id"))
126 .and_then(|v| v.as_str());
127 if let Some(id) = id {
128 self.genes.insert(id.to_string(), gene.clone());
129 }
130 }
131 }
132
133 if let Some(quarantines) = pulled.get("quarantines").and_then(|v| v.as_array()) {
135 for qid in quarantines {
136 if let Some(qid) = qid.as_str() {
137 self.genes.remove(qid);
138 }
139 }
140 }
141
142 if let Some(edges) = pulled.get("edges").and_then(|v| v.as_array()) {
144 for edge in edges {
145 let key = edge.get("signal_key").or_else(|| edge.get("signalKey"))
146 .and_then(|v| v.as_str());
147 let gene_id = edge.get("gene_id").or_else(|| edge.get("geneId"))
148 .and_then(|v| v.as_str()).unwrap_or("");
149 if let Some(key) = key {
150 let list = self.edges.entry(key.to_string()).or_default();
151 let mut found = false;
152 for existing in list.iter_mut() {
153 let eid = existing.get("gene_id").or_else(|| existing.get("geneId"))
154 .and_then(|v| v.as_str()).unwrap_or("");
155 if eid == gene_id {
156 *existing = edge.clone();
157 found = true;
158 break;
159 }
160 }
161 if !found {
162 list.push(edge.clone());
163 }
164 }
165 }
166 }
167
168 let gp = pulled.get("globalPrior").or_else(|| pulled.get("global_prior"));
170 if let Some(gp) = gp.and_then(|v| v.as_object()) {
171 for (key, val) in gp {
172 if let Some(obj) = val.as_object() {
173 let alpha = obj.get("alpha").and_then(|v| v.as_f64()).unwrap_or(1.0);
174 let beta = obj.get("beta").and_then(|v| v.as_f64()).unwrap_or(1.0);
175 self.global_prior.insert(key.clone(), (alpha, beta));
176 }
177 }
178 }
179
180 if let Some(cur) = pulled.get("cursor").and_then(|v| v.as_u64()) {
181 self.cursor = cur;
182 }
183 }
184
185 pub fn load_delta(&mut self, delta: &serde_json::Value) {
187 self.apply_delta(delta);
188 }
189
190 pub fn select_gene(&self, signals: &[SignalTag]) -> GeneSelectionResult {
193 if self.genes.is_empty() {
194 return GeneSelectionResult {
195 action: "none".to_string(),
196 gene_id: None,
197 gene: None,
198 strategy: None,
199 confidence: 0.0,
200 coverage_score: None,
201 alternatives: vec![],
202 reason: "no genes in cache".to_string(),
203 from_cache: true,
204 };
205 }
206
207 let signal_keys: Vec<&str> = signals.iter().map(|s| s.signal_type.as_str()).collect();
208
209 struct Candidate {
210 gene: serde_json::Value,
211 rank_score: f64,
212 coverage_score: f64,
213 }
214 let mut candidates: Vec<Candidate> = Vec::new();
215
216 for gene in self.genes.values() {
217 if gene.get("visibility").and_then(|v| v.as_str()) == Some("quarantined") {
219 continue;
220 }
221
222 let gene_signal_types = self.extract_gene_signal_types(gene);
224 if gene_signal_types.is_empty() {
225 continue;
226 }
227
228 let match_count = signal_keys.iter()
230 .filter(|k| gene_signal_types.iter().any(|gs| gs == *k))
231 .count();
232 let coverage_score = match_count as f64 / gene_signal_types.len() as f64;
233 if coverage_score == 0.0 {
234 continue;
235 }
236
237 let sc = gene.get("success_count").or_else(|| gene.get("successCount"))
239 .and_then(|v| v.as_f64()).unwrap_or(0.0);
240 let fc = gene.get("failure_count").or_else(|| gene.get("failureCount"))
241 .and_then(|v| v.as_f64()).unwrap_or(0.0);
242 let mut alpha = sc + 1.0;
243 let mut beta = fc + 1.0;
244
245 for key in &signal_keys {
247 if let Some(&(pa, pb)) = self.global_prior.get(*key) {
248 alpha += 0.3 * pa;
249 beta += 0.3 * pb;
250 }
251 }
252
253 let sampled_score = alpha / (alpha + beta);
254
255 let total_obs = sc + fc;
257 if total_obs >= 10.0 && sc / total_obs < 0.18 {
258 continue;
259 }
260
261 let rank_score = coverage_score * 0.4 + sampled_score * 0.6;
263
264 candidates.push(Candidate {
265 gene: gene.clone(),
266 rank_score,
267 coverage_score,
268 });
269 }
270
271 if candidates.is_empty() {
272 return GeneSelectionResult {
273 action: "create_suggested".to_string(),
274 gene_id: None,
275 gene: None,
276 strategy: None,
277 confidence: 0.0,
278 coverage_score: None,
279 alternatives: vec![],
280 reason: "no matching genes for signals".to_string(),
281 from_cache: true,
282 };
283 }
284
285 candidates.sort_by(|a, b| b.rank_score.partial_cmp(&a.rank_score).unwrap_or(std::cmp::Ordering::Equal));
287
288 let best = &candidates[0];
289
290 let limit = std::cmp::min(candidates.len(), 4);
292 let alternatives: Vec<Alternative> = candidates[1..limit].iter().map(|c| {
293 Alternative {
294 gene_id: c.gene.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
295 confidence: round_to_2(c.rank_score),
296 title: c.gene.get("title").and_then(|v| v.as_str()).map(|s| s.to_string()),
297 }
298 }).collect();
299
300 let strategy = best.gene.get("strategy").and_then(|v| v.as_array()).map(|arr| {
302 arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()
303 });
304
305 let gene_id = best.gene.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
306
307 GeneSelectionResult {
308 action: "apply_gene".to_string(),
309 gene_id: Some(gene_id),
310 gene: Some(best.gene.clone()),
311 strategy,
312 confidence: round_to_2(best.rank_score),
313 coverage_score: Some(round_to_2(best.coverage_score)),
314 alternatives,
315 reason: format!("local cache selection ({} genes)", self.genes.len()),
316 from_cache: true,
317 }
318 }
319
320 fn extract_gene_signal_types(&self, gene: &serde_json::Value) -> Vec<String> {
323 let raw = gene.get("signals_match").or_else(|| gene.get("signalsMatch"));
324 match raw.and_then(|v| v.as_array()) {
325 Some(arr) => arr.iter().filter_map(|s| {
326 if let Some(str_val) = s.as_str() {
327 Some(str_val.to_string())
328 } else if let Some(obj) = s.as_object() {
329 obj.get("type").and_then(|v| v.as_str()).map(|s| s.to_string())
330 } else {
331 None
332 }
333 }).collect(),
334 None => vec![],
335 }
336 }
337}
338
339impl Default for EvolutionCache {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345fn round_to_2(v: f64) -> f64 {
346 (v * 100.0 + 0.5).floor() / 100.0
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use serde_json::json;
353
354 fn make_signal(signal_type: &str) -> SignalTag {
355 SignalTag {
356 signal_type: signal_type.to_string(),
357 provider: None,
358 stage: None,
359 severity: None,
360 }
361 }
362
363 fn make_snapshot(genes: Vec<serde_json::Value>, edges: Vec<serde_json::Value>) -> serde_json::Value {
364 json!({
365 "genes": genes,
366 "edges": edges,
367 "cursor": 42,
368 })
369 }
370
371 fn gene_json(id: &str, signals: &[&str], success: i64, failure: i64) -> serde_json::Value {
372 json!({
373 "id": id,
374 "category": "test",
375 "title": format!("Gene {}", id),
376 "signals_match": signals,
377 "strategy": ["try something"],
378 "visibility": "public",
379 "success_count": success,
380 "failure_count": failure,
381 })
382 }
383
384 #[test]
387 fn new_cache_is_empty() {
388 let cache = EvolutionCache::new();
389 assert_eq!(cache.gene_count(), 0);
390 assert_eq!(cache.cursor(), 0);
391 }
392
393 #[test]
394 fn default_cache_is_empty() {
395 let cache = EvolutionCache::default();
396 assert_eq!(cache.gene_count(), 0);
397 }
398
399 #[test]
400 fn load_snapshot_populates_genes() {
401 let mut cache = EvolutionCache::new();
402 let snapshot = make_snapshot(
403 vec![gene_json("g1", &["error:timeout"], 5, 1)],
404 vec![],
405 );
406 cache.load_snapshot(&snapshot);
407 assert_eq!(cache.gene_count(), 1);
408 assert_eq!(cache.cursor(), 42);
409 }
410
411 #[test]
412 fn load_snapshot_replaces_existing() {
413 let mut cache = EvolutionCache::new();
414 cache.load_snapshot(&make_snapshot(
415 vec![gene_json("g1", &["error:timeout"], 5, 1)],
416 vec![],
417 ));
418 assert_eq!(cache.gene_count(), 1);
419
420 cache.load_snapshot(&make_snapshot(
421 vec![
422 gene_json("g2", &["error:dns_error"], 3, 0),
423 gene_json("g3", &["error:crash"], 1, 1),
424 ],
425 vec![],
426 ));
427 assert_eq!(cache.gene_count(), 2);
428 }
429
430 #[test]
431 fn load_snapshot_with_gene_id_key() {
432 let mut cache = EvolutionCache::new();
433 let snapshot = json!({
434 "genes": [{"gene_id": "alt-1", "signals_match": ["error:timeout"]}],
435 "cursor": 10,
436 });
437 cache.load_snapshot(&snapshot);
438 assert_eq!(cache.gene_count(), 1);
439 }
440
441 #[test]
444 fn select_gene_empty_cache_returns_none() {
445 let cache = EvolutionCache::new();
446 let result = cache.select_gene(&[make_signal("error:timeout")]);
447 assert_eq!(result.action, "none");
448 assert!(result.gene_id.is_none());
449 assert!(result.from_cache);
450 }
451
452 #[test]
453 fn select_gene_matching_signal() {
454 let mut cache = EvolutionCache::new();
455 cache.load_snapshot(&make_snapshot(
456 vec![gene_json("g1", &["error:timeout"], 10, 1)],
457 vec![],
458 ));
459 let result = cache.select_gene(&[make_signal("error:timeout")]);
460 assert_eq!(result.action, "apply_gene");
461 assert_eq!(result.gene_id.as_deref(), Some("g1"));
462 assert!(result.confidence > 0.0);
463 assert!(result.from_cache);
464 }
465
466 #[test]
467 fn select_gene_no_matching_signal() {
468 let mut cache = EvolutionCache::new();
469 cache.load_snapshot(&make_snapshot(
470 vec![gene_json("g1", &["error:timeout"], 10, 1)],
471 vec![],
472 ));
473 let result = cache.select_gene(&[make_signal("error:crash")]);
474 assert_eq!(result.action, "create_suggested");
475 }
476
477 #[test]
478 fn select_gene_skips_quarantined() {
479 let mut cache = EvolutionCache::new();
480 let mut gene = gene_json("g1", &["error:timeout"], 10, 1);
481 gene["visibility"] = json!("quarantined");
482 cache.load_snapshot(&make_snapshot(vec![gene], vec![]));
483 let result = cache.select_gene(&[make_signal("error:timeout")]);
484 assert_eq!(result.action, "create_suggested");
485 }
486
487 #[test]
488 fn select_gene_ban_threshold() {
489 let mut cache = EvolutionCache::new();
490 cache.load_snapshot(&make_snapshot(
492 vec![gene_json("g1", &["error:timeout"], 1, 9)],
493 vec![],
494 ));
495 let result = cache.select_gene(&[make_signal("error:timeout")]);
496 assert_eq!(result.action, "create_suggested");
497 }
498
499 #[test]
500 fn select_gene_picks_best_of_multiple() {
501 let mut cache = EvolutionCache::new();
502 cache.load_snapshot(&make_snapshot(
503 vec![
504 gene_json("g-low", &["error:timeout"], 2, 5),
505 gene_json("g-high", &["error:timeout"], 20, 1),
506 ],
507 vec![],
508 ));
509 let result = cache.select_gene(&[make_signal("error:timeout")]);
510 assert_eq!(result.action, "apply_gene");
511 assert_eq!(result.gene_id.as_deref(), Some("g-high"));
512 }
513
514 #[test]
515 fn select_gene_returns_alternatives() {
516 let mut cache = EvolutionCache::new();
517 cache.load_snapshot(&make_snapshot(
518 vec![
519 gene_json("g1", &["error:timeout"], 20, 1),
520 gene_json("g2", &["error:timeout"], 15, 2),
521 gene_json("g3", &["error:timeout"], 10, 3),
522 ],
523 vec![],
524 ));
525 let result = cache.select_gene(&[make_signal("error:timeout")]);
526 assert_eq!(result.action, "apply_gene");
527 assert!(result.alternatives.len() >= 1);
528 }
529
530 #[test]
531 fn select_gene_returns_strategy() {
532 let mut cache = EvolutionCache::new();
533 cache.load_snapshot(&make_snapshot(
534 vec![gene_json("g1", &["error:timeout"], 10, 1)],
535 vec![],
536 ));
537 let result = cache.select_gene(&[make_signal("error:timeout")]);
538 assert!(result.strategy.is_some());
539 assert!(!result.strategy.unwrap().is_empty());
540 }
541
542 #[test]
545 fn apply_delta_adds_genes() {
546 let mut cache = EvolutionCache::new();
547 cache.load_snapshot(&make_snapshot(
548 vec![gene_json("g1", &["error:timeout"], 5, 1)],
549 vec![],
550 ));
551 assert_eq!(cache.gene_count(), 1);
552
553 let delta = json!({
554 "pulled": {
555 "genes": [{"id": "g2", "signals_match": ["error:crash"], "success_count": 3, "failure_count": 0}],
556 "cursor": 50,
557 }
558 });
559 cache.apply_delta(&delta);
560 assert_eq!(cache.gene_count(), 2);
561 assert_eq!(cache.cursor(), 50);
562 }
563
564 #[test]
565 fn apply_delta_quarantines_genes() {
566 let mut cache = EvolutionCache::new();
567 cache.load_snapshot(&make_snapshot(
568 vec![
569 gene_json("g1", &["error:timeout"], 5, 1),
570 gene_json("g2", &["error:crash"], 3, 0),
571 ],
572 vec![],
573 ));
574 assert_eq!(cache.gene_count(), 2);
575
576 let delta = json!({
577 "pulled": {
578 "quarantines": ["g1"],
579 "cursor": 55,
580 }
581 });
582 cache.apply_delta(&delta);
583 assert_eq!(cache.gene_count(), 1);
584 }
585
586 #[test]
587 fn load_delta_is_alias_for_apply_delta() {
588 let mut cache = EvolutionCache::new();
589 let delta = json!({
590 "pulled": {
591 "genes": [{"id": "g1", "signals_match": ["error:timeout"]}],
592 "cursor": 10,
593 }
594 });
595 cache.load_delta(&delta);
596 assert_eq!(cache.gene_count(), 1);
597 assert_eq!(cache.cursor(), 10);
598 }
599
600 #[test]
603 fn load_snapshot_with_global_prior() {
604 let mut cache = EvolutionCache::new();
605 let snapshot = json!({
606 "genes": [{"id": "g1", "signals_match": ["error:timeout"], "success_count": 5, "failure_count": 1}],
607 "globalPrior": {
608 "error:timeout": {"alpha": 10.0, "beta": 2.0},
609 },
610 "cursor": 1,
611 });
612 cache.load_snapshot(&snapshot);
613 let result = cache.select_gene(&[make_signal("error:timeout")]);
615 assert_eq!(result.action, "apply_gene");
616 assert!(result.confidence > 0.5);
618 }
619
620 #[test]
623 fn round_to_2_basic() {
624 assert_eq!(round_to_2(0.555), 0.56);
625 assert_eq!(round_to_2(0.0), 0.0);
626 }
627
628 #[test]
629 fn round_to_2_already_round() {
630 assert_eq!(round_to_2(0.50), 0.50);
632 assert_eq!(round_to_2(1.0), 1.0);
633 }
634
635 #[test]
638 fn select_gene_coverage_score_present() {
639 let mut cache = EvolutionCache::new();
640 cache.load_snapshot(&make_snapshot(
641 vec![gene_json("g1", &["error:timeout"], 10, 1)],
642 vec![],
643 ));
644 let result = cache.select_gene(&[make_signal("error:timeout")]);
645 assert!(result.coverage_score.is_some());
646 assert!(result.coverage_score.unwrap() > 0.0);
647 }
648
649 #[test]
652 fn thompson_sampling_more_successes_higher_confidence() {
653 let mut cache = EvolutionCache::new();
654 cache.load_snapshot(&make_snapshot(
655 vec![
656 gene_json("g-weak", &["error:timeout"], 3, 3),
657 gene_json("g-strong", &["error:timeout"], 50, 2),
658 ],
659 vec![],
660 ));
661 let result = cache.select_gene(&[make_signal("error:timeout")]);
662 assert_eq!(result.action, "apply_gene");
663 assert_eq!(result.gene_id.as_deref(), Some("g-strong"));
664 assert!(result.confidence > 0.5);
666 assert_eq!(result.alternatives.len(), 1);
668 assert_eq!(result.alternatives[0].gene_id, "g-weak");
669 assert!(result.confidence > result.alternatives[0].confidence);
670 }
671
672 #[test]
673 fn thompson_sampling_global_prior_boosts_confidence() {
674 let mut cache_no_prior = EvolutionCache::new();
676 cache_no_prior.load_snapshot(&make_snapshot(
677 vec![gene_json("g1", &["error:timeout"], 5, 5)],
678 vec![],
679 ));
680 let result_no_prior = cache_no_prior.select_gene(&[make_signal("error:timeout")]);
681
682 let mut cache_with_prior = EvolutionCache::new();
684 let snapshot = json!({
685 "genes": [gene_json("g1", &["error:timeout"], 5, 5)],
686 "globalPrior": {
687 "error:timeout": {"alpha": 50.0, "beta": 1.0},
688 },
689 "cursor": 1,
690 });
691 cache_with_prior.load_snapshot(&snapshot);
692 let result_with_prior = cache_with_prior.select_gene(&[make_signal("error:timeout")]);
693
694 assert!(result_with_prior.confidence > result_no_prior.confidence);
696 }
697
698 #[test]
701 fn load_snapshot_with_edges() {
702 let mut cache = EvolutionCache::new();
703 let snapshot = json!({
704 "genes": [
705 gene_json("g1", &["error:timeout"], 10, 1),
706 ],
707 "edges": [
708 {"signal_key": "error:timeout", "gene_id": "g1", "weight": 0.9},
709 {"signalKey": "error:crash", "geneId": "g1", "weight": 0.5},
710 ],
711 "cursor": 100,
712 });
713 cache.load_snapshot(&snapshot);
714 assert_eq!(cache.gene_count(), 1);
715 assert_eq!(cache.cursor(), 100);
716 }
717
718 #[test]
719 fn apply_delta_updates_edges() {
720 let mut cache = EvolutionCache::new();
721 cache.load_snapshot(&make_snapshot(
722 vec![gene_json("g1", &["error:timeout"], 10, 1)],
723 vec![],
724 ));
725
726 let delta = json!({
727 "pulled": {
728 "edges": [
729 {"signal_key": "error:timeout", "gene_id": "g1", "weight": 0.95},
730 ],
731 "cursor": 60,
732 }
733 });
734 cache.apply_delta(&delta);
735 assert_eq!(cache.cursor(), 60);
736 }
737
738 #[test]
739 fn apply_delta_updates_existing_gene() {
740 let mut cache = EvolutionCache::new();
741 cache.load_snapshot(&make_snapshot(
742 vec![gene_json("g1", &["error:timeout"], 5, 1)],
743 vec![],
744 ));
745
746 let delta = json!({
748 "pulled": {
749 "genes": [{"id": "g1", "signals_match": ["error:timeout"], "success_count": 20, "failure_count": 1}],
750 "cursor": 70,
751 }
752 });
753 cache.apply_delta(&delta);
754 assert_eq!(cache.gene_count(), 1);
756
757 let result = cache.select_gene(&[make_signal("error:timeout")]);
758 assert_eq!(result.action, "apply_gene");
759 assert_eq!(result.gene_id.as_deref(), Some("g1"));
760 }
761
762 #[test]
763 fn apply_delta_updates_global_prior() {
764 let mut cache = EvolutionCache::new();
765 cache.load_snapshot(&make_snapshot(
766 vec![gene_json("g1", &["error:timeout"], 5, 5)],
767 vec![],
768 ));
769 let result_before = cache.select_gene(&[make_signal("error:timeout")]);
770
771 let delta = json!({
772 "pulled": {
773 "globalPrior": {
774 "error:timeout": {"alpha": 100.0, "beta": 1.0},
775 },
776 "cursor": 80,
777 }
778 });
779 cache.apply_delta(&delta);
780 let result_after = cache.select_gene(&[make_signal("error:timeout")]);
781
782 assert!(result_after.confidence >= result_before.confidence);
784 }
785
786 #[test]
789 fn select_gene_multi_signal_coverage() {
790 let mut cache = EvolutionCache::new();
791 cache.load_snapshot(&make_snapshot(
792 vec![
793 gene_json("g1", &["error:timeout"], 10, 1),
795 gene_json("g2", &["error:timeout", "error:crash"], 8, 1),
797 ],
798 vec![],
799 ));
800 let result = cache.select_gene(&[
801 make_signal("error:timeout"),
802 make_signal("error:crash"),
803 ]);
804 assert_eq!(result.action, "apply_gene");
805 assert!(result.coverage_score.is_some());
808 }
809
810 #[test]
811 fn select_gene_no_signal_types_on_gene() {
812 let mut cache = EvolutionCache::new();
813 let snapshot = json!({
815 "genes": [{"id": "g1", "signals_match": [], "success_count": 10, "failure_count": 0}],
816 "cursor": 1,
817 });
818 cache.load_snapshot(&snapshot);
819 let result = cache.select_gene(&[make_signal("error:timeout")]);
820 assert_eq!(result.action, "create_suggested");
822 }
823
824 #[test]
827 fn select_gene_with_object_signal_format() {
828 let mut cache = EvolutionCache::new();
829 let snapshot = json!({
830 "genes": [{
831 "id": "g1",
832 "signals_match": [{"type": "error:timeout"}],
833 "success_count": 10,
834 "failure_count": 1,
835 }],
836 "cursor": 1,
837 });
838 cache.load_snapshot(&snapshot);
839 let result = cache.select_gene(&[make_signal("error:timeout")]);
840 assert_eq!(result.action, "apply_gene");
841 }
842}