fix(cli): empty query emits error.v1 invalid_input for search + ask (Bug #14)
이전: `kebab search "" --json` / `kebab search " " --json` / `kebab ask "" --json` 모두 exit=0 + silent 0 hit (search) 또는 LLM 빈 prompt round-trip (ask). user mistake (typo, shell expansion 실수) 가 silent → debugging 비용. 이후: 양쪽 arm 에서 `query.trim().is_empty()` → kebab_app::StructuredError (ErrorV1, code=invalid_input, hint 포함). exit=2 (StructuredError → 기존 exit_code() 의 generic non-zero path). --bulk mode 는 영향 0 (bulk arm 이 query 무시). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -819,6 +819,17 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
|
||||
// p9-fb-42: bulk mode requires no query; single-query mode requires query.
|
||||
let query_text = match query.as_ref() {
|
||||
Some(q) if q.trim().is_empty() => {
|
||||
return Err(anyhow::Error::new(kebab_app::StructuredError(
|
||||
kebab_app::ErrorV1 {
|
||||
schema_version: kebab_app::ERROR_V1_ID.to_string(),
|
||||
code: "invalid_input".to_string(),
|
||||
message: "query is empty; provide a non-empty search term or use --bulk".into(),
|
||||
details: serde_json::Value::Null,
|
||||
hint: Some("e.g. `kebab search 'rust async'` or `kebab search --bulk < queries.ndjson`".into()),
|
||||
},
|
||||
)));
|
||||
}
|
||||
Some(q) => q.clone(),
|
||||
None => {
|
||||
return Err(anyhow::anyhow!("query is required unless --bulk is set"));
|
||||
@@ -988,6 +999,17 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
multi_hop,
|
||||
} => {
|
||||
let cfg = kebab_config::Config::load(cli.config.as_deref())?;
|
||||
if query.trim().is_empty() {
|
||||
return Err(anyhow::Error::new(kebab_app::StructuredError(
|
||||
kebab_app::ErrorV1 {
|
||||
schema_version: kebab_app::ERROR_V1_ID.to_string(),
|
||||
code: "invalid_input".to_string(),
|
||||
message: "query is empty; provide a non-empty prompt".into(),
|
||||
details: serde_json::Value::Null,
|
||||
hint: Some("e.g. `kebab ask \"explain this code\"`".into()),
|
||||
},
|
||||
)));
|
||||
}
|
||||
if *stream {
|
||||
// p9-fb-33: streaming branch. Background thread runs
|
||||
// ask_with_config (which calls into the rag pipeline);
|
||||
|
||||
47
crates/kebab-cli/tests/cli_empty_query.rs
Normal file
47
crates/kebab-cli/tests/cli_empty_query.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Integration tests for Bug #14: empty or whitespace-only query must emit
|
||||
//! error.v1 code=invalid_input and exit nonzero (not silent 0-hit return).
|
||||
|
||||
use std::process::Command;
|
||||
use serde_json::Value;
|
||||
|
||||
fn kebab_bin() -> String {
|
||||
env!("CARGO_BIN_EXE_kebab").to_string()
|
||||
}
|
||||
|
||||
fn parse_error_v1(stderr: &str) -> Value {
|
||||
let last = stderr.lines().last().expect("expected error.v1 ndjson on stderr");
|
||||
serde_json::from_str(last)
|
||||
.unwrap_or_else(|e| panic!("expected ndjson on stderr: {e}\nstderr={stderr}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_empty_query_emits_invalid_input() {
|
||||
for q in ["", " "] {
|
||||
let out = Command::new(kebab_bin())
|
||||
.args(["search", q, "--json"])
|
||||
.output()
|
||||
.expect("spawn kebab");
|
||||
assert_ne!(
|
||||
out.status.code(),
|
||||
Some(0),
|
||||
"empty/whitespace query must fail (q={q:?})"
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let v = parse_error_v1(&stderr);
|
||||
assert_eq!(v["schema_version"], "error.v1", "stderr={stderr}");
|
||||
assert_eq!(v["code"], "invalid_input", "stderr={stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_empty_query_emits_invalid_input() {
|
||||
let out = Command::new(kebab_bin())
|
||||
.args(["ask", "", "--json"])
|
||||
.output()
|
||||
.expect("spawn kebab");
|
||||
assert_ne!(out.status.code(), Some(0));
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let v = parse_error_v1(&stderr);
|
||||
assert_eq!(v["schema_version"], "error.v1");
|
||||
assert_eq!(v["code"], "invalid_input");
|
||||
}
|
||||
Reference in New Issue
Block a user