kanban_cli/error.rs
1use kanban_domain::KanbanError;
2use thiserror::Error;
3
4/// CLI-boundary error type.
5///
6/// Wraps [`KanbanError`] for the domain layer, plus CLI-specific
7/// concerns (handler-built messages, IO, serialization). Handlers
8/// that return `Result<T, KanbanCliError>` propagate all failure
9/// modes uniformly with `?`, and the dispatcher converts to the JSON
10/// `CliResponse` envelope at the boundary.
11#[derive(Error, Debug)]
12pub enum KanbanCliError {
13 #[error(transparent)]
14 Domain(#[from] KanbanError),
15 /// Handler-built user-facing message at the CLI boundary.
16 ///
17 /// Named to match the MCP-side `KanbanMcpError::Resolution` so
18 /// the two surfaces stay symmetric. Used when a handler has
19 /// enough input context to enrich an otherwise anonymous domain
20 /// error (`cycle detected: making A a parent of B would create a
21 /// cycle`). Identifier-resolution failures flow through `Domain`
22 /// directly so the structured
23 /// [`kanban_domain::DomainError::NotFoundByName`] / `Ambiguous`
24 /// variants stay introspectable.
25 ///
26 /// `Display` renders the hint verbatim, no wrapper prefix,
27 /// matching the established CLI convention used by `card get` /
28 /// `card delete` / `card archive`.
29 #[error("{hint}")]
30 Resolution { hint: String },
31 #[error(transparent)]
32 Io(#[from] std::io::Error),
33 #[error(transparent)]
34 Serialization(#[from] serde_json::Error),
35}
36
37pub type KanbanCliResult<T> = Result<T, KanbanCliError>;
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42
43 #[test]
44 fn test_from_kanban_error_lands_in_domain_variant() {
45 let domain = KanbanError::validation("bad input");
46 let cli: KanbanCliError = domain.into();
47 assert!(matches!(cli, KanbanCliError::Domain(_)));
48 }
49
50 #[test]
51 fn test_resolution_variant_displays_hint_verbatim() {
52 let err = KanbanCliError::Resolution {
53 hint: "no card matches 'foo'".into(),
54 };
55 assert!(err.to_string().contains("foo"));
56 }
57
58 /// CLI error messages must match the existing convention used by
59 /// `card get` / `card delete` / `card archive` / `card update`:
60 /// just the hint string with no wrapper prefix. The Resolution
61 /// variant's Display renders only the hint.
62 #[test]
63 fn test_resolution_variant_display_has_no_prefix() {
64 let hint = "cycle detected: making KAN-5 a parent of KAN-7 would create a cycle";
65 let err = KanbanCliError::Resolution { hint: hint.into() };
66 assert_eq!(err.to_string(), hint);
67 }
68}