fix(config): emit error.v1 code=config_not_found for missing --config path (Bug #10)
이전: `kebab search "rust" --config /tmp/nonexistent.toml --json` 가 exit=0 +
`{"hits":[]}` silent fallback to XDG default. typo / wrong path 가 0-hit 으로만
surface — debugging nightmare.
이후: kebab_config::ConfigNotFound thiserror::Error 추가, Config::load 의
`Some(p) if !p.exists()` arm 이 anyhow::Error::new(ConfigNotFound { path })
return. kebab_app::error_wire::classify 가 downcast → ErrorV1 code=config_not_found,
hint, details.path 채워서 stderr 에 ndjson 으로 emit.
R-1 (relative path): std::path::Path::exists() 는 cwd-relative — 별도 작업 없이
absolute + relative 모두 cover. integration test 두 개로 검증.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
crates/kebab-cli/tests/cli_config_not_found.rs
Normal file
51
crates/kebab-cli/tests/cli_config_not_found.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Integration tests for Bug #10: explicit --config <path> that does not exist
|
||||
//! must fail with exit≠0 and error.v1 code=config_not_found (not silently fall
|
||||
//! back to XDG defaults).
|
||||
|
||||
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 invalid_config_path_emits_error_v1_with_nonzero_exit() {
|
||||
let absent = "/tmp/__kebab_bugfix3_absolute_nonexistent.toml";
|
||||
assert!(!std::path::Path::new(absent).exists());
|
||||
|
||||
let out = Command::new(kebab_bin())
|
||||
.args(["search", "rust", "--config", absent, "--json"])
|
||||
.output()
|
||||
.expect("spawn kebab");
|
||||
|
||||
assert_ne!(out.status.code(), Some(0), "exit must be nonzero on missing --config");
|
||||
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"], "config_not_found");
|
||||
assert!(v["hint"].is_string(), "hint must be present");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_relative_config_path_emits_config_not_found() {
|
||||
// Bug #10 spec §6 R-1: relative path も cwd-relative で cover.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let out = Command::new(kebab_bin())
|
||||
.args(["search", "rust", "--config", "nonexistent-rel.toml", "--json"])
|
||||
.current_dir(tmp.path())
|
||||
.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"], "config_not_found");
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
//! on stderr while non-json mode emits the legacy `error:` text prefix.
|
||||
//!
|
||||
//! The `config_invalid` code is triggered by supplying an *existing* but
|
||||
//! malformed TOML file via `--config`. Note: supplying a *non-existent*
|
||||
//! path does NOT trigger this error — Config::load silently falls back to
|
||||
//! defaults when the specified config file is absent (by design, so that
|
||||
//! `kebab doctor` runs before `kebab init` is ever called). A file that
|
||||
//! exists but fails TOML parsing is the reliable path to `config_invalid`.
|
||||
//! malformed TOML file via `--config`. A file that exists but fails TOML
|
||||
//! parsing is the reliable path to `config_invalid`. Supplying a path that
|
||||
//! does not exist emits `config_not_found` instead (Bug #10 fix, v0.20.0
|
||||
//! bugfix3); see `cli_config_not_found.rs` for those tests.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user