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:
2026-05-27 23:14:54 +00:00
parent 760eee89c8
commit 28f513795e
6 changed files with 99 additions and 8 deletions

View File

@@ -24,6 +24,15 @@ pub struct ConfigInvalid {
pub cause: String,
}
/// p20-bugfix3 Bug #10: explicit `--config <path>` was missing → silent
/// fallback to defaults instead of fail-fast. `kebab-app::error_wire::classify`
/// downcasts → `code: "config_not_found"` ErrorV1.
#[derive(Debug, thiserror::Error)]
#[error("config file does not exist: {path}")]
pub struct ConfigNotFound {
pub path: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub schema_version: u32,
@@ -688,7 +697,11 @@ impl Config {
pub fn load(path: Option<&Path>) -> anyhow::Result<Self> {
let from_disk = match path {
Some(p) if p.exists() => Self::from_file(p)?,
Some(_) => Self::defaults(),
Some(p) => {
return Err(anyhow::Error::new(ConfigNotFound {
path: p.to_path_buf(),
}));
}
None => {
let p = Self::xdg_config_path();
if p.exists() {
@@ -1817,4 +1830,17 @@ mod fb27_tests {
signal.cause
);
}
#[test]
fn config_load_explicit_nonexistent_path_returns_config_not_found() {
// Bug #10: --config /tmp/nonexistent.toml → silent fallback 금지.
let p = std::path::Path::new("/tmp/__kebab_bugfix3_nonexistent.toml");
assert!(!p.exists(), "test precondition: path must not exist");
let err = Config::load(Some(p)).expect_err("expected ConfigNotFound");
let signal = err
.downcast_ref::<ConfigNotFound>()
.expect("from_load error should downcast to ConfigNotFound");
assert_eq!(signal.path, p.to_path_buf());
}
}