diff --git a/crates/eureka-state/src/bin/huginn/main.rs b/crates/eureka-state/src/bin/huginn/main.rs index 8b6f5c2..9f6cb10 100644 --- a/crates/eureka-state/src/bin/huginn/main.rs +++ b/crates/eureka-state/src/bin/huginn/main.rs @@ -99,8 +99,11 @@ enum Command { }, /// Search the public forge repos' code and docs by meaning. Sources { - /// What to find, in words. - text: String, + /// What to find, in words: one to eight texts. Each is a topic; the + /// ranked lists of several are fused, so a brief with many topics + /// retrieves by every one. + #[arg(required = true, num_args = 1..=8)] + texts: Vec, /// Look in this repo alone (its forge name). #[arg(long)] repo: Option, @@ -275,12 +278,12 @@ fn run(settings: &Settings, command: Command) -> Result { query(settings, selection, semantic, None, documents) } Command::Sources { - text, + texts, repo, front, top_k, json, - } => sources(settings, text, repo, front, top_k, json), + } => sources(settings, texts, repo, front, top_k, json), Command::Recipe(Recipe::Blocked { spec_id, page }) => { blocked(settings, &spec_id, page.cursor, page.documents) } @@ -438,15 +441,15 @@ fn blocked( fn sources( settings: &Settings, - text: String, + texts: Vec, repo: Option, front: bool, top_k: Option, json: bool, ) -> Result { let query = SourceQuery { - text: Line(text), - texts: vec![], + text: Line::default(), + texts: texts.into_iter().map(Line).collect(), top_k: top_k.unwrap_or(DEFAULT_TOP_K), repo: repo.map(Short), front, diff --git a/crates/eureka-state/tests/cli.rs b/crates/eureka-state/tests/cli.rs index 97a8afc..89e652a 100644 --- a/crates/eureka-state/tests/cli.rs +++ b/crates/eureka-state/tests/cli.rs @@ -1383,6 +1383,31 @@ fn sources_front_prints_only_the_front_page_with_its_lines() { assert_eq!(found.out.trim_end(), "Demo:README.md:1-2 @abcdef1 0.900\n# Demo", "{}", found.out); } +/// `huginn sources ...` sends every text in `texts` and none in `text`, the +/// daemon fuses them into one answer, and a ninth or no text never leaves the CLI. +#[test] +fn sources_sends_each_text_as_a_topic_and_prints_the_fused_answer() { + let (server, seen) = replying( + "search_sources", + HuginnMindResponse::Sources(huginn_mind::SourceHits { indexed_under: huginn_mind::SourceModel { name: "m".into(), digest: "d".into() }, hits: vec![] }), + ); + let ran = at(server.addr, &["sources", "qdrant filter", "reciprocal rank", "--top-k", "5"], None); + assert_eq!(ran.code, 0, "{}", ran.err); + let HuginnMindRequest::SearchSources { query, .. } = seen.lock().unwrap().remove(0) else { panic!("a source search") }; + let texts: Vec<&str> = query.texts.iter().map(|text| text.0.as_str()).collect(); + assert_eq!((texts, query.text.0.as_str(), query.top_k), (vec!["qdrant filter", "reciprocal rank"], "", 5)); + + let daemon = source_daemon("d1", "d1"); + let found = at(daemon.addr, &["sources", "cursor", "readme", "--repo", "Demo"], None); + assert_eq!(found.code, 0, "{}", found.err); + assert!(found.out.contains("Demo:src/lib.rs:3-9") && found.out.contains("Demo:README.md:1-2"), "{}", found.out); + + let nine = ["sources", "a", "b", "c", "d", "e", "f", "g", "h", "i"]; + assert_ne!(at(server.addr, &nine, None).code, 0); + assert_ne!(at(server.addr, &["sources"], None).code, 0); + assert!(seen.lock().unwrap().is_empty(), "a refused command asked nothing"); +} + /// A daemon whose embedder is not the model the index was built under refuses /// the search naming both, and the CLI exits 1 with that word. #[test]