aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-nucleo/src/tests.rs
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2026-03-16 16:28:54 -0700
committerGitHub <noreply@github.com>2026-03-16 16:28:54 -0700
commita964c27db2a359233bad200a64696b663eca4be5 (patch)
tree9370c6f7b541b79d7183dd754a9d6a863f51c1e2 /crates/atuin-nucleo/src/tests.rs
parentfeat: Allow headless account ops against Hub server (#3280) (diff)
parentvendor nucleo fork into atuin workspace (diff)
downloadatuin-a964c27db2a359233bad200a64696b663eca4be5.zip
chore: vendor nucleo-ext + fork, so we can depend on our changes properly (#3284)
We cannot publish to crates.io without specifying a version, and we cannot do that without properly forking nucleo. We're shipping atuin-nucleo, but will likely drop this if we can get our changes upstream. This is highlighted in the README + manifest, and the original author is still included. Originally forked here: https://github.com/atuinsh/nucleo-ext cc @BinaryMuse - this should just be a vendor + restructure, but would appreciate the sanity check ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing
Diffstat (limited to 'crates/atuin-nucleo/src/tests.rs')
-rw-r--r--crates/atuin-nucleo/src/tests.rs267
1 files changed, 267 insertions, 0 deletions
diff --git a/crates/atuin-nucleo/src/tests.rs b/crates/atuin-nucleo/src/tests.rs
new file mode 100644
index 00000000..1052264a
--- /dev/null
+++ b/crates/atuin-nucleo/src/tests.rs
@@ -0,0 +1,267 @@
+use std::sync::Arc;
+
+use atuin_nucleo_matcher::Config;
+
+use crate::{pattern, Nucleo};
+
+#[test]
+fn active_injector_count() {
+ let mut nucleo: Nucleo<()> = Nucleo::new(Config::DEFAULT, Arc::new(|| ()), Some(1), 1);
+ assert_eq!(nucleo.active_injectors(), 0);
+ let injector = nucleo.injector();
+ assert_eq!(nucleo.active_injectors(), 1);
+ let injector2 = nucleo.injector();
+ assert_eq!(nucleo.active_injectors(), 2);
+ drop(injector2);
+ assert_eq!(nucleo.active_injectors(), 1);
+ nucleo.restart(false);
+ assert_eq!(nucleo.active_injectors(), 0);
+ let injector3 = nucleo.injector();
+ assert_eq!(nucleo.active_injectors(), 1);
+ nucleo.tick(0);
+ assert_eq!(nucleo.active_injectors(), 1);
+ drop(injector);
+ assert_eq!(nucleo.active_injectors(), 1);
+ drop(injector3);
+ assert_eq!(nucleo.active_injectors(), 0);
+}
+
+#[derive(Clone, Debug)]
+struct TestItem {
+ text: String,
+ category: u32,
+ priority: u32,
+}
+
+#[test]
+fn filter_excludes_items() {
+ let mut nucleo: Nucleo<TestItem> = Nucleo::new(Config::DEFAULT, Arc::new(|| ()), Some(1), 1);
+ let injector = nucleo.injector();
+
+ // Add items with different categories
+ injector.push(
+ TestItem {
+ text: "apple".into(),
+ category: 1,
+ priority: 10,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "apricot".into(),
+ category: 2,
+ priority: 20,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "avocado".into(),
+ category: 1,
+ priority: 30,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+
+ // Search without filter - should get all 3
+ nucleo.pattern.reparse(
+ 0,
+ "a",
+ pattern::CaseMatching::Ignore,
+ pattern::Normalization::Smart,
+ false,
+ );
+ while nucleo.tick(10).running {}
+ assert_eq!(nucleo.snapshot().matched_item_count(), 3);
+
+ // Set filter to only include category 1
+ nucleo.set_filter(Some(Arc::new(|item: &TestItem| item.category == 1)));
+
+ // Search again - should only get 2 items (apple, avocado)
+ while nucleo.tick(10).running {}
+ assert_eq!(nucleo.snapshot().matched_item_count(), 2);
+
+ // Verify the items are correct
+ let items: Vec<_> = nucleo
+ .snapshot()
+ .matched_items(..)
+ .map(|i| i.data.text.clone())
+ .collect();
+ assert!(items.contains(&"apple".to_string()));
+ assert!(items.contains(&"avocado".to_string()));
+ assert!(!items.contains(&"apricot".to_string()));
+
+ // Remove filter - should get all 3 again
+ nucleo.set_filter(None);
+ while nucleo.tick(10).running {}
+ assert_eq!(nucleo.snapshot().matched_item_count(), 3);
+}
+
+#[test]
+fn scorer_affects_sort_order() {
+ let mut nucleo: Nucleo<TestItem> = Nucleo::new(Config::DEFAULT, Arc::new(|| ()), Some(1), 1);
+ let injector = nucleo.injector();
+
+ // Add items with different priorities
+ injector.push(
+ TestItem {
+ text: "banana".into(),
+ category: 1,
+ priority: 10,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "blueberry".into(),
+ category: 1,
+ priority: 100,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "blackberry".into(),
+ category: 1,
+ priority: 50,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+
+ // Search without scorer - results sorted by fuzzy score
+ nucleo.pattern.reparse(
+ 0,
+ "b",
+ pattern::CaseMatching::Ignore,
+ pattern::Normalization::Smart,
+ false,
+ );
+ while nucleo.tick(10).running {}
+ assert_eq!(nucleo.snapshot().matched_item_count(), 3);
+
+ // Set scorer that uses priority as the score (ignoring fuzzy score)
+ nucleo.set_scorer(Some(Arc::new(|item: &TestItem, _fuzzy_score| {
+ item.priority
+ })));
+
+ // Search again - should be sorted by priority (high to low)
+ while nucleo.tick(10).running {}
+ let items: Vec<_> = nucleo
+ .snapshot()
+ .matched_items(..)
+ .map(|i| i.data.clone())
+ .collect();
+ assert_eq!(items.len(), 3);
+ assert_eq!(items[0].text, "blueberry"); // priority 100
+ assert_eq!(items[1].text, "blackberry"); // priority 50
+ assert_eq!(items[2].text, "banana"); // priority 10
+
+ // Verify external_score is set correctly
+ let matches = nucleo.snapshot().matches();
+ assert_eq!(matches[0].external_score, 100);
+ assert_eq!(matches[1].external_score, 50);
+ assert_eq!(matches[2].external_score, 10);
+}
+
+#[test]
+fn filter_and_scorer_combined() {
+ let mut nucleo: Nucleo<TestItem> = Nucleo::new(Config::DEFAULT, Arc::new(|| ()), Some(1), 1);
+ let injector = nucleo.injector();
+
+ injector.push(
+ TestItem {
+ text: "cherry".into(),
+ category: 1,
+ priority: 10,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "cranberry".into(),
+ category: 2,
+ priority: 100,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "coconut".into(),
+ category: 1,
+ priority: 50,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+
+ // Set both filter (category 1) and scorer (priority)
+ nucleo.set_filter(Some(Arc::new(|item: &TestItem| item.category == 1)));
+ nucleo.set_scorer(Some(Arc::new(|item: &TestItem, _| item.priority)));
+
+ nucleo.pattern.reparse(
+ 0,
+ "c",
+ pattern::CaseMatching::Ignore,
+ pattern::Normalization::Smart,
+ false,
+ );
+ while nucleo.tick(10).running {}
+
+ // Should have 2 items (cherry, coconut) sorted by priority
+ let items: Vec<_> = nucleo
+ .snapshot()
+ .matched_items(..)
+ .map(|i| i.data.clone())
+ .collect();
+ assert_eq!(items.len(), 2);
+ assert_eq!(items[0].text, "coconut"); // priority 50
+ assert_eq!(items[1].text, "cherry"); // priority 10
+}
+
+#[test]
+fn scorer_combines_with_fuzzy_score() {
+ let mut nucleo: Nucleo<TestItem> = Nucleo::new(Config::DEFAULT, Arc::new(|| ()), Some(1), 1);
+ let injector = nucleo.injector();
+
+ injector.push(
+ TestItem {
+ text: "date".into(),
+ category: 1,
+ priority: 100,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+ injector.push(
+ TestItem {
+ text: "dragon fruit".into(),
+ category: 1,
+ priority: 10,
+ },
+ |item, cols| cols[0] = item.text.clone().into(),
+ );
+
+ // Set scorer that combines fuzzy score with priority
+ nucleo.set_scorer(Some(Arc::new(|item: &TestItem, fuzzy_score| {
+ fuzzy_score + item.priority
+ })));
+
+ nucleo.pattern.reparse(
+ 0,
+ "d",
+ pattern::CaseMatching::Ignore,
+ pattern::Normalization::Smart,
+ false,
+ );
+ while nucleo.tick(10).running {}
+
+ // Both items match, verify that external_score includes priority boost
+ let matches = nucleo.snapshot().matches();
+ assert_eq!(matches.len(), 2);
+
+ // The raw fuzzy scores should be in Match.score
+ // The combined scores should be in Match.external_score
+ for m in matches {
+ let item = nucleo.snapshot().get_item(m.idx).unwrap();
+ assert_eq!(m.external_score, m.score + item.data.priority);
+ }
+}