Skip to main content

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}