huginn sources takes one to eight texts

The CLI sends every positional text as `texts` and never `text`; help says each
is a topic and the lists are fused. Daemon and CLI deploy together. Cut
gamecult-chat:cut_spec:cut-source-query-texts.r1, commit 4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-11 16:04:37 +00:00
commit 6f66e048a5
2 changed files with 35 additions and 7 deletions

View file

@ -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<String>,
/// Look in this repo alone (its forge name).
#[arg(long)]
repo: Option<String>,
@ -275,12 +278,12 @@ fn run(settings: &Settings, command: Command) -> Result<u8, Trouble> {
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<String>,
repo: Option<String>,
front: bool,
top_k: Option<u32>,
json: bool,
) -> Result<u8, Trouble> {
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,

View file

@ -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 <TEXT>...` 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]