1use std::collections::HashMap;
11
12use crate::tree::{NodeKind, TrimNode};
13
14#[derive(Debug, Clone, Default)]
19pub struct ItemMetadata {
20 pub activity: Option<f64>,
22 pub days_since_update: Option<f64>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum TrimStrategyKind {
29 ElementCount,
32 Cascading,
35 SizeProportional,
38 ThreadLevel,
41 HeadTail,
44 Default,
46 Random,
48 Reversed,
51 Priority,
54}
55
56impl TrimStrategyKind {
57 pub fn parse(s: &str) -> Option<Self> {
59 match s {
60 "element_count" => Some(Self::ElementCount),
61 "cascading" => Some(Self::Cascading),
62 "size_proportional" => Some(Self::SizeProportional),
63 "thread_level" => Some(Self::ThreadLevel),
64 "head_tail" => Some(Self::HeadTail),
65 "default" => Some(Self::Default),
66 "random" => Some(Self::Random),
67 "reversed" => Some(Self::Reversed),
68 "priority" => Some(Self::Priority),
69 _ => None,
70 }
71 }
72
73 pub fn as_str(&self) -> &'static str {
75 match self {
76 Self::ElementCount => "element_count",
77 Self::Cascading => "cascading",
78 Self::SizeProportional => "size_proportional",
79 Self::ThreadLevel => "thread_level",
80 Self::HeadTail => "head_tail",
81 Self::Default => "default",
82 Self::Random => "random",
83 Self::Reversed => "reversed",
84 Self::Priority => "priority",
85 }
86 }
87}
88
89pub trait TrimStrategy {
95 fn assign_values(&self, tree: &mut TrimNode);
97}
98
99pub struct ElementCountStrategy;
101
102impl TrimStrategy for ElementCountStrategy {
103 fn assign_values(&self, tree: &mut TrimNode) {
104 let n = tree.children.len();
105 if n == 0 {
106 return;
107 }
108 for (i, child) in tree.children.iter_mut().enumerate() {
109 child.value = 1.0 - (i as f64 / n as f64) * 0.7;
111 for grandchild in &mut child.children {
113 grandchild.value = child.value;
114 }
115 }
116 }
117}
118
119pub struct CascadingStrategy {
122 pub beta: f64,
124}
125
126impl Default for CascadingStrategy {
127 fn default() -> Self {
128 Self { beta: 0.95 }
129 }
130}
131
132impl TrimStrategy for CascadingStrategy {
133 fn assign_values(&self, tree: &mut TrimNode) {
134 let n = tree.children.len();
135 if n == 0 {
136 return;
137 }
138 for (i, child) in tree.children.iter_mut().enumerate() {
139 child.value = self.beta.powi((n - 1 - i) as i32);
141 for grandchild in &mut child.children {
142 grandchild.value = child.value;
143 }
144 }
145 }
146}
147
148pub struct SizeProportionalStrategy;
150
151impl SizeProportionalStrategy {
152 fn file_type_weight(path: &str) -> f64 {
154 let path_lower = path.to_lowercase();
155 if path_lower.ends_with(".lock")
156 || path_lower.ends_with(".sum")
157 || path_lower.ends_with("package-lock.json")
158 || path_lower.ends_with("yarn.lock")
159 {
160 0.05
161 } else if path_lower.ends_with(".min.js") || path_lower.ends_with(".min.css") {
162 0.10
163 } else if path_lower.contains("migration") || path_lower.contains("schema") {
164 0.60
165 } else if path_lower.contains("test") || path_lower.contains("spec") {
166 0.70
167 } else {
168 1.00
169 }
170 }
171}
172
173impl TrimStrategy for SizeProportionalStrategy {
174 fn assign_values(&self, tree: &mut TrimNode) {
175 for child in &mut tree.children {
176 let type_weight = if let NodeKind::Item { .. } = &child.kind {
180 1.0 } else {
183 1.0
184 };
185 child.value = type_weight;
186 for grandchild in &mut child.children {
187 grandchild.value = type_weight;
188 }
189 }
190 }
191}
192
193pub fn assign_diff_values(tree: &mut TrimNode, file_paths: &[&str]) {
195 for (child, path) in tree.children.iter_mut().zip(file_paths.iter()) {
196 let type_weight = SizeProportionalStrategy::file_type_weight(path);
197 child.value = type_weight;
198 for grandchild in &mut child.children {
199 grandchild.value = type_weight;
200 }
201 }
202}
203
204pub struct ThreadLevelStrategy;
207
208impl TrimStrategy for ThreadLevelStrategy {
209 fn assign_values(&self, tree: &mut TrimNode) {
210 for child in &mut tree.children {
211 child.value = 1.0; let comment_count = child.children.len();
217 for (j, comment) in child.children.iter_mut().enumerate() {
218 if j == 0 || j == comment_count - 1 {
219 comment.value = 1.0; } else {
221 comment.value = 0.5; }
223 }
224 }
225 }
226}
227
228pub fn assign_discussion_values(tree: &mut TrimNode, resolved: &[bool]) {
230 for (child, &is_resolved) in tree.children.iter_mut().zip(resolved.iter()) {
231 let disc_value = if is_resolved { 0.3 } else { 1.0 };
232 child.value = disc_value;
233
234 let comment_count = child.children.len();
235 for (j, comment) in child.children.iter_mut().enumerate() {
236 if j == 0 || j == comment_count - 1 {
237 comment.value = disc_value; } else {
239 comment.value = disc_value * 0.5;
240 }
241 }
242 }
243}
244
245pub struct HeadTailStrategy;
247
248impl TrimStrategy for HeadTailStrategy {
249 fn assign_values(&self, tree: &mut TrimNode) {
250 let n = tree.children.len();
251 if n == 0 {
252 return;
253 }
254 let head_end = (n as f64 * 0.30).ceil() as usize;
255 let tail_start = n - (n as f64 * 0.70).ceil() as usize;
256
257 for (i, child) in tree.children.iter_mut().enumerate() {
258 if i < head_end {
259 child.value = 0.8; } else if i >= tail_start {
261 child.value = 1.0; } else {
263 child.value = 0.1; }
265 }
266 }
267}
268
269pub struct DefaultStrategy;
271
272impl TrimStrategy for DefaultStrategy {
273 fn assign_values(&self, tree: &mut TrimNode) {
274 set_uniform_value(tree, 1.0);
275 }
276}
277
278fn set_uniform_value(node: &mut TrimNode, value: f64) {
279 node.value = value;
280 for child in &mut node.children {
281 set_uniform_value(child, value);
282 }
283}
284
285pub struct RandomStrategy {
290 pub seed: u64,
292}
293
294impl Default for RandomStrategy {
295 fn default() -> Self {
296 Self { seed: 42 }
297 }
298}
299
300impl TrimStrategy for RandomStrategy {
301 fn assign_values(&self, tree: &mut TrimNode) {
302 let n = tree.children.len();
303 if n == 0 {
304 return;
305 }
306 let mut state = self.seed;
308 for child in tree.children.iter_mut() {
309 state = state
310 .wrapping_mul(6364136223846793005)
311 .wrapping_add(1442695040888963407);
312 child.value = (state >> 33) as f64 / (u32::MAX as f64);
314 for grandchild in &mut child.children {
315 grandchild.value = child.value;
316 }
317 }
318 }
319}
320
321pub struct ReversedStrategy;
326
327impl TrimStrategy for ReversedStrategy {
328 fn assign_values(&self, tree: &mut TrimNode) {
329 let n = tree.children.len();
330 if n == 0 {
331 return;
332 }
333 let denom = (n - 1).max(1) as f64;
334 for (i, child) in tree.children.iter_mut().enumerate() {
335 child.value = 0.3 + (i as f64 / denom) * 0.7;
337 for grandchild in &mut child.children {
338 grandchild.value = child.value;
339 }
340 }
341 }
342}
343
344pub struct PriorityStrategy;
351
352impl TrimStrategy for PriorityStrategy {
353 fn assign_values(&self, tree: &mut TrimNode) {
354 ElementCountStrategy.assign_values(tree);
356 }
357}
358
359pub fn assign_priority_values(tree: &mut TrimNode, metadata: &[ItemMetadata]) {
364 let n = tree.children.len();
365 if n == 0 {
366 return;
367 }
368
369 let activities: Vec<f64> = metadata
371 .iter()
372 .enumerate()
373 .map(|(i, m)| m.activity.unwrap_or(1.0 - i as f64 / n as f64))
374 .collect();
375
376 let recencies: Vec<f64> = metadata
377 .iter()
378 .enumerate()
379 .map(|(i, m)| {
380 m.days_since_update
382 .map(|d| 1.0 / (1.0 + d / 30.0)) .unwrap_or(1.0 - i as f64 / n as f64)
384 })
385 .collect();
386
387 let norm = |vals: &[f64]| -> Vec<f64> {
389 let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
390 let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
391 let range = (max - min).max(1e-9);
392 vals.iter().map(|v| (v - min) / range).collect()
393 };
394
395 let act_norm = norm(&activities);
396 let rec_norm = norm(&recencies);
397
398 const W_POS: f64 = 0.40;
399 const W_ACT: f64 = 0.35;
400 const W_REC: f64 = 0.25;
401
402 for (i, child) in tree.children.iter_mut().enumerate() {
403 let pos_score = 1.0 - (i as f64 / n as f64); let act_score = act_norm.get(i).copied().unwrap_or(0.5);
405 let rec_score = rec_norm.get(i).copied().unwrap_or(0.5);
406 child.value = W_POS * pos_score + W_ACT * act_score + W_REC * rec_score;
407 for grandchild in &mut child.children {
408 grandchild.value = child.value;
409 }
410 }
411}
412
413pub fn create_strategy(kind: TrimStrategyKind) -> Box<dyn TrimStrategy> {
419 match kind {
420 TrimStrategyKind::ElementCount => Box::new(ElementCountStrategy),
421 TrimStrategyKind::Cascading => Box::new(CascadingStrategy::default()),
422 TrimStrategyKind::SizeProportional => Box::new(SizeProportionalStrategy),
423 TrimStrategyKind::ThreadLevel => Box::new(ThreadLevelStrategy),
424 TrimStrategyKind::HeadTail => Box::new(HeadTailStrategy),
425 TrimStrategyKind::Default => Box::new(DefaultStrategy),
426 TrimStrategyKind::Random => Box::new(RandomStrategy::default()),
427 TrimStrategyKind::Reversed => Box::new(ReversedStrategy),
428 TrimStrategyKind::Priority => Box::new(PriorityStrategy),
429 }
430}
431
432fn hardcoded_defaults() -> HashMap<&'static str, TrimStrategyKind> {
438 let mut m = HashMap::new();
439 m.insert("get_issues", TrimStrategyKind::Priority);
440 m.insert("get_issue_comments", TrimStrategyKind::Cascading);
441 m.insert("get_merge_requests", TrimStrategyKind::Priority);
442 m.insert(
443 "get_merge_request_diffs",
444 TrimStrategyKind::SizeProportional,
445 );
446 m.insert(
447 "get_merge_request_discussions",
448 TrimStrategyKind::ThreadLevel,
449 );
450 m.insert("get_job_logs", TrimStrategyKind::HeadTail);
451 m.insert("get_pipeline", TrimStrategyKind::Default);
452 m.insert("get_users", TrimStrategyKind::Default);
453 m.insert("get_statuses", TrimStrategyKind::Default);
454 m
455}
456
457pub struct StrategyResolver {
465 hardcoded: HashMap<&'static str, TrimStrategyKind>,
466 overrides: HashMap<String, TrimStrategyKind>,
467 proxy_strip_enabled: bool,
468}
469
470impl StrategyResolver {
471 pub fn new() -> Self {
473 Self {
474 hardcoded: hardcoded_defaults(),
475 overrides: HashMap::new(),
476 proxy_strip_enabled: true,
477 }
478 }
479
480 pub fn with_overrides(overrides: HashMap<String, TrimStrategyKind>) -> Self {
482 Self {
483 hardcoded: hardcoded_defaults(),
484 overrides,
485 proxy_strip_enabled: true,
486 }
487 }
488
489 pub fn set_proxy_strip(&mut self, enabled: bool) {
491 self.proxy_strip_enabled = enabled;
492 }
493
494 pub fn resolve(&self, tool_name: &str) -> TrimStrategyKind {
496 if let Some(&kind) = self.overrides.get(tool_name) {
498 return kind;
499 }
500
501 if let Some(&kind) = self.hardcoded.get(tool_name) {
503 return kind;
504 }
505
506 if self.proxy_strip_enabled
508 && let Some(stripped) = strip_proxy_prefix(tool_name)
509 {
510 if let Some(&kind) = self.overrides.get(stripped) {
511 return kind;
512 }
513 if let Some(&kind) = self.hardcoded.get(stripped) {
514 return kind;
515 }
516 }
517
518 TrimStrategyKind::Default
520 }
521}
522
523impl Default for StrategyResolver {
524 fn default() -> Self {
525 Self::new()
526 }
527}
528
529fn strip_proxy_prefix(tool_name: &str) -> Option<&str> {
532 tool_name.find("__").map(|pos| &tool_name[pos + 2..])
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
542 fn test_resolver_hardcoded_defaults() {
543 let resolver = StrategyResolver::new();
544 assert_eq!(resolver.resolve("get_issues"), TrimStrategyKind::Priority);
546 assert_eq!(
547 resolver.resolve("get_merge_requests"),
548 TrimStrategyKind::Priority
549 );
550 assert_eq!(
551 resolver.resolve("get_issue_comments"),
552 TrimStrategyKind::Cascading
553 );
554 assert_eq!(
555 resolver.resolve("get_merge_request_diffs"),
556 TrimStrategyKind::SizeProportional
557 );
558 assert_eq!(
559 resolver.resolve("get_merge_request_discussions"),
560 TrimStrategyKind::ThreadLevel
561 );
562 assert_eq!(resolver.resolve("get_job_logs"), TrimStrategyKind::HeadTail);
563 assert_eq!(resolver.resolve("get_pipeline"), TrimStrategyKind::Default);
564 }
565
566 #[test]
567 fn test_resolver_unknown_tool_fallback() {
568 let resolver = StrategyResolver::new();
569 assert_eq!(resolver.resolve("unknown_tool"), TrimStrategyKind::Default);
570 }
571
572 #[test]
573 fn test_resolver_override_takes_priority() {
574 let mut overrides = HashMap::new();
575 overrides.insert("get_issues".into(), TrimStrategyKind::HeadTail);
576 let resolver = StrategyResolver::with_overrides(overrides);
577 assert_eq!(resolver.resolve("get_issues"), TrimStrategyKind::HeadTail);
578 }
579
580 #[test]
581 fn test_resolver_proxy_strip() {
582 let resolver = StrategyResolver::new();
583 assert_eq!(
584 resolver.resolve("cloud__get_issues"),
585 TrimStrategyKind::Priority
586 );
587 assert_eq!(
588 resolver.resolve("jira_proxy__get_issue_comments"),
589 TrimStrategyKind::Cascading
590 );
591 }
592
593 #[test]
594 fn test_resolver_proxy_strip_with_override() {
595 let mut overrides = HashMap::new();
596 overrides.insert("get_tasks".into(), TrimStrategyKind::ElementCount);
597 let resolver = StrategyResolver::with_overrides(overrides);
598 assert_eq!(
599 resolver.resolve("cloud__get_tasks"),
600 TrimStrategyKind::ElementCount
601 );
602 }
603
604 #[test]
605 fn test_resolver_proxy_strip_disabled() {
606 let mut resolver = StrategyResolver::new();
607 resolver.set_proxy_strip(false);
608 assert_eq!(
609 resolver.resolve("cloud__get_issues"),
610 TrimStrategyKind::Default
611 );
612 }
613
614 #[test]
615 fn test_strip_proxy_prefix() {
616 assert_eq!(strip_proxy_prefix("cloud__get_issues"), Some("get_issues"));
617 assert_eq!(
618 strip_proxy_prefix("jira__get_issue_comments"),
619 Some("get_issue_comments")
620 );
621 assert_eq!(strip_proxy_prefix("get_issues"), None);
622 assert_eq!(strip_proxy_prefix("no_prefix"), None);
623 }
624
625 #[test]
628 fn test_strategy_kind_from_str() {
629 assert_eq!(
630 TrimStrategyKind::parse("element_count"),
631 Some(TrimStrategyKind::ElementCount)
632 );
633 assert_eq!(
634 TrimStrategyKind::parse("cascading"),
635 Some(TrimStrategyKind::Cascading)
636 );
637 assert_eq!(TrimStrategyKind::parse("unknown"), None);
638 }
639
640 #[test]
641 fn test_strategy_kind_round_trip() {
642 let kinds = [
643 TrimStrategyKind::ElementCount,
644 TrimStrategyKind::Cascading,
645 TrimStrategyKind::SizeProportional,
646 TrimStrategyKind::ThreadLevel,
647 TrimStrategyKind::HeadTail,
648 TrimStrategyKind::Default,
649 ];
650 for kind in &kinds {
651 assert_eq!(TrimStrategyKind::parse(kind.as_str()), Some(*kind));
652 }
653 }
654
655 #[test]
658 fn test_element_count_strategy() {
659 let mut tree = make_test_tree(5);
660 ElementCountStrategy.assign_values(&mut tree);
661
662 assert!(tree.children[0].value > tree.children[4].value);
664 assert!((tree.children[0].value - 1.0).abs() < 0.01);
665 assert!(tree.children[4].value >= 0.3);
666 }
667
668 #[test]
669 fn test_cascading_strategy() {
670 let mut tree = make_test_tree(10);
671 CascadingStrategy::default().assign_values(&mut tree);
672
673 assert!(tree.children[9].value > tree.children[0].value);
675 assert!(tree.children[0].value < 0.7);
677 }
678
679 #[test]
680 fn test_head_tail_strategy() {
681 let mut tree = make_test_tree(100);
682 HeadTailStrategy.assign_values(&mut tree);
683
684 assert!((tree.children[0].value - 0.8).abs() < 0.01);
686 assert!((tree.children[29].value - 0.8).abs() < 0.01);
687 assert!((tree.children[99].value - 1.0).abs() < 0.01);
689 let mut tree2 = make_test_tree(200);
692 HeadTailStrategy.assign_values(&mut tree2);
693 assert!(tree2.children[0].value < tree2.children[199].value);
697 }
698
699 #[test]
700 fn test_default_strategy() {
701 let mut tree = make_test_tree(5);
702 DefaultStrategy.assign_values(&mut tree);
703
704 for child in &tree.children {
705 assert!((child.value - 1.0).abs() < 0.001);
706 }
707 }
708
709 #[test]
710 fn test_assign_diff_values() {
711 let mut tree = make_test_tree(3);
712 let paths = ["Cargo.lock", "src/main.rs", "test_helper.rs"];
713 assign_diff_values(&mut tree, &paths);
714
715 assert!((tree.children[0].value - 0.05).abs() < 0.01); assert!((tree.children[1].value - 1.0).abs() < 0.01); assert!((tree.children[2].value - 0.70).abs() < 0.01); }
719
720 #[test]
721 fn test_assign_discussion_values() {
722 let mut tree = TrimNode::new(0, NodeKind::Root, 0);
723 for i in 0..3 {
724 let mut disc = TrimNode::new(i + 1, NodeKind::Item { index: i }, 10);
725 for j in 0..3 {
726 let comment = TrimNode::new(10 + i * 3 + j, NodeKind::Item { index: j }, 5);
727 disc.children.push(comment);
728 }
729 tree.children.push(disc);
730 }
731
732 let resolved = [true, false, true];
733 assign_discussion_values(&mut tree, &resolved);
734
735 assert!((tree.children[0].value - 0.3).abs() < 0.01); assert!((tree.children[1].value - 1.0).abs() < 0.01); assert!((tree.children[2].value - 0.3).abs() < 0.01); }
739
740 #[test]
743 fn test_size_proportional_strategy() {
744 let mut tree = make_test_tree(3);
745 SizeProportionalStrategy.assign_values(&mut tree);
746 for child in &tree.children {
748 assert!((child.value - 1.0).abs() < 0.01);
749 }
750 }
751
752 #[test]
753 fn test_file_type_weights() {
754 assert!((SizeProportionalStrategy::file_type_weight("Cargo.lock") - 0.05).abs() < 0.01);
755 assert!(
756 (SizeProportionalStrategy::file_type_weight("package-lock.json") - 0.05).abs() < 0.01
757 );
758 assert!((SizeProportionalStrategy::file_type_weight("yarn.lock") - 0.05).abs() < 0.01);
759 assert!((SizeProportionalStrategy::file_type_weight("go.sum") - 0.05).abs() < 0.01);
760 assert!((SizeProportionalStrategy::file_type_weight("app.min.js") - 0.10).abs() < 0.01);
761 assert!((SizeProportionalStrategy::file_type_weight("style.min.css") - 0.10).abs() < 0.01);
762 assert!(
763 (SizeProportionalStrategy::file_type_weight("db/migration_001.sql") - 0.60).abs()
764 < 0.01
765 );
766 assert!((SizeProportionalStrategy::file_type_weight("schema.prisma") - 0.60).abs() < 0.01);
767 assert!((SizeProportionalStrategy::file_type_weight("test_main.rs") - 0.70).abs() < 0.01);
768 assert!((SizeProportionalStrategy::file_type_weight("main.spec.ts") - 0.70).abs() < 0.01);
769 assert!((SizeProportionalStrategy::file_type_weight("src/main.rs") - 1.0).abs() < 0.01);
770 }
771
772 #[test]
775 fn test_thread_level_strategy() {
776 let mut tree = TrimNode::new(0, NodeKind::Root, 0);
777 for i in 0..2 {
778 let mut disc = TrimNode::new(i + 1, NodeKind::Item { index: i }, 10);
779 for j in 0..4 {
780 let comment = TrimNode::new(10 + i * 4 + j, NodeKind::Item { index: j }, 5);
781 disc.children.push(comment);
782 }
783 tree.children.push(disc);
784 }
785
786 ThreadLevelStrategy.assign_values(&mut tree);
787
788 assert!((tree.children[0].children[0].value - 1.0).abs() < 0.01);
790 assert!((tree.children[0].children[3].value - 1.0).abs() < 0.01);
791 assert!((tree.children[0].children[1].value - 0.5).abs() < 0.01);
793 assert!((tree.children[0].children[2].value - 0.5).abs() < 0.01);
794 }
795
796 #[test]
799 fn test_create_strategy_all_kinds() {
800 let kinds = [
801 TrimStrategyKind::ElementCount,
802 TrimStrategyKind::Cascading,
803 TrimStrategyKind::SizeProportional,
804 TrimStrategyKind::ThreadLevel,
805 TrimStrategyKind::HeadTail,
806 TrimStrategyKind::Default,
807 TrimStrategyKind::Random,
808 TrimStrategyKind::Reversed,
809 TrimStrategyKind::Priority,
810 ];
811 for kind in &kinds {
812 let strategy = create_strategy(*kind);
813 let mut tree = make_test_tree(5);
814 strategy.assign_values(&mut tree);
815 }
817 }
818
819 #[test]
822 fn test_strategies_on_empty_tree() {
823 let strategies: Vec<Box<dyn TrimStrategy>> = vec![
824 Box::new(ElementCountStrategy),
825 Box::new(CascadingStrategy::default()),
826 Box::new(SizeProportionalStrategy),
827 Box::new(ThreadLevelStrategy),
828 Box::new(HeadTailStrategy),
829 Box::new(DefaultStrategy),
830 Box::new(RandomStrategy::default()),
831 Box::new(ReversedStrategy),
832 Box::new(PriorityStrategy),
833 ];
834 for strategy in &strategies {
835 let mut tree = TrimNode::new(0, NodeKind::Root, 0);
836 strategy.assign_values(&mut tree); }
838 }
839
840 #[test]
843 fn test_assign_diff_values_various_types() {
844 let mut tree = make_test_tree(5);
845 let paths = [
846 "Cargo.lock",
847 "bundle.min.js",
848 "db/migration_v2.sql",
849 "src/tests/test_auth.rs",
850 "src/lib.rs",
851 ];
852 assign_diff_values(&mut tree, &paths);
853
854 assert!(tree.children[0].value < tree.children[4].value); assert!(tree.children[1].value < tree.children[4].value); }
857
858 fn make_test_tree(n: usize) -> TrimNode {
861 let mut root = TrimNode::new(0, NodeKind::Root, 0);
862 for i in 0..n {
863 let node = TrimNode::new(i + 1, NodeKind::Item { index: i }, 10);
864 root.children.push(node);
865 }
866 root
867 }
868
869 #[test]
872 fn test_random_strategy_reproducible() {
873 let mut tree1 = make_test_tree(5);
874 let mut tree2 = make_test_tree(5);
875 RandomStrategy { seed: 42 }.assign_values(&mut tree1);
876 RandomStrategy { seed: 42 }.assign_values(&mut tree2);
877 for (a, b) in tree1.children.iter().zip(tree2.children.iter()) {
879 assert!((a.value - b.value).abs() < 1e-9);
880 }
881 }
882
883 #[test]
884 fn test_random_strategy_different_seeds() {
885 let mut tree1 = make_test_tree(5);
886 let mut tree2 = make_test_tree(5);
887 RandomStrategy { seed: 42 }.assign_values(&mut tree1);
888 RandomStrategy { seed: 99 }.assign_values(&mut tree2);
889 let same = tree1
891 .children
892 .iter()
893 .zip(tree2.children.iter())
894 .all(|(a, b)| (a.value - b.value).abs() < 1e-9);
895 assert!(!same, "Different seeds should produce different orderings");
896 }
897
898 #[test]
899 fn test_random_strategy_values_in_range() {
900 let mut tree = make_test_tree(20);
901 RandomStrategy::default().assign_values(&mut tree);
902 for child in &tree.children {
903 assert!(child.value >= 0.0 && child.value <= 1.0);
904 }
905 }
906
907 #[test]
908 fn test_reversed_strategy() {
909 let mut tree = make_test_tree(5);
910 ReversedStrategy.assign_values(&mut tree);
911 let last = tree.children.last().unwrap().value;
913 let first = tree.children.first().unwrap().value;
914 assert!(
915 last > first,
916 "Reversed: last item should have highest value"
917 );
918 assert!((last - 1.0).abs() < 0.01, "Last item should be ≈ 1.0");
919 assert!((first - 0.3).abs() < 0.1, "First item should be ≈ 0.3");
920 }
921
922 #[test]
923 fn test_reversed_is_mirror_of_element_count() {
924 let n = 10;
925 let mut tree_ec = make_test_tree(n);
926 let mut tree_rev = make_test_tree(n);
927 ElementCountStrategy.assign_values(&mut tree_ec);
928 ReversedStrategy.assign_values(&mut tree_rev);
929 for i in 0..n - 1 {
932 assert!(
933 tree_ec.children[i].value >= tree_ec.children[i + 1].value,
934 "ElementCount should be non-increasing"
935 );
936 assert!(
937 tree_rev.children[i].value <= tree_rev.children[i + 1].value,
938 "Reversed should be non-decreasing"
939 );
940 }
941 assert!((tree_ec.children[0].value - 1.0).abs() < 0.01);
943 assert!((tree_rev.children[n - 1].value - 1.0).abs() < 0.01);
944 assert!((tree_ec.children[n - 1].value - 0.3).abs() < 0.1);
945 assert!((tree_rev.children[0].value - 0.3).abs() < 0.01);
946 }
947
948 #[test]
949 fn test_priority_strategy_fallback_to_element_count() {
950 let mut tree_ec = make_test_tree(5);
951 let mut tree_pr = make_test_tree(5);
952 ElementCountStrategy.assign_values(&mut tree_ec);
953 PriorityStrategy.assign_values(&mut tree_pr);
954 for (ec, pr) in tree_ec.children.iter().zip(tree_pr.children.iter()) {
956 assert!((ec.value - pr.value).abs() < 0.01);
957 }
958 }
959
960 #[test]
961 fn test_assign_priority_values_with_metadata() {
962 let mut tree = make_test_tree(3);
963 let metadata = vec![
965 ItemMetadata {
966 activity: Some(1.0),
967 days_since_update: Some(90.0),
968 },
969 ItemMetadata {
970 activity: Some(2.0),
971 days_since_update: Some(30.0),
972 },
973 ItemMetadata {
974 activity: Some(10.0),
975 days_since_update: Some(1.0),
976 },
977 ];
978 assign_priority_values(&mut tree, &metadata);
979 assert!(
980 tree.children[2].value > tree.children[0].value,
981 "Highly active recent item should score higher"
982 );
983 }
984
985 #[test]
986 fn test_priority_strategy_round_trip() {
987 assert_eq!(
988 TrimStrategyKind::parse("random"),
989 Some(TrimStrategyKind::Random)
990 );
991 assert_eq!(
992 TrimStrategyKind::parse("reversed"),
993 Some(TrimStrategyKind::Reversed)
994 );
995 assert_eq!(
996 TrimStrategyKind::parse("priority"),
997 Some(TrimStrategyKind::Priority)
998 );
999 assert_eq!(TrimStrategyKind::Random.as_str(), "random");
1000 assert_eq!(TrimStrategyKind::Reversed.as_str(), "reversed");
1001 assert_eq!(TrimStrategyKind::Priority.as_str(), "priority");
1002 }
1003
1004 #[test]
1005 fn test_hardcoded_defaults_use_priority_for_issues() {
1006 let resolver = StrategyResolver::new();
1007 assert_eq!(resolver.resolve("get_issues"), TrimStrategyKind::Priority);
1008 assert_eq!(
1009 resolver.resolve("get_merge_requests"),
1010 TrimStrategyKind::Priority
1011 );
1012 assert_eq!(
1014 resolver.resolve("get_issue_comments"),
1015 TrimStrategyKind::Cascading
1016 );
1017 }
1018}