use std::borrow::Cow;
use std::fmt;
use santh_error::{ErrorLocation, SanthError, SanthErrorContract};
#[derive(Debug)]
enum DomainError {
ShapeMismatch { want: usize, got: usize },
NotFound(String),
}
impl fmt::Display for DomainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DomainError::ShapeMismatch { want, got } => {
write!(f, "tensor shape mismatch: wanted {want}, got {got}")
}
DomainError::NotFound(id) => write!(f, "node {id} not found"),
}
}
}
impl std::error::Error for DomainError {}
impl SanthErrorContract for DomainError {
fn error_code(&self) -> &'static str {
match self {
DomainError::ShapeMismatch { .. } => "DOMAIN-E001",
DomainError::NotFound(_) => "DOMAIN-E002",
}
}
fn fix_hint(&self) -> Cow<'_, str> {
match self {
DomainError::ShapeMismatch { .. } => {
Cow::Borrowed("Fix: Reshape the input tensor to match the expected dimensions.")
}
DomainError::NotFound(_) => {
Cow::Borrowed("Fix: Verify the node id exists in the graph before referencing it.")
}
}
}
}
#[test]
fn domain_error_renders_code_title_and_fix_via_contract() {
let err = DomainError::ShapeMismatch { want: 4, got: 3 };
assert_eq!(err.error_code(), "DOMAIN-E001");
let msg = err.actionable_message();
assert!(
msg.contains("tensor shape mismatch: wanted 4, got 3"),
"title must come from Display: {msg}"
);
assert!(
msg.contains("Fix: Reshape the input tensor"),
"fix hint must be present: {msg}"
);
}
#[test]
fn domain_error_defaults_empty_context_and_no_location() {
let err = DomainError::NotFound("n7".to_string());
assert!(err.context().is_empty());
assert!(err.location().is_none());
assert!(err.actionable_message().contains("node n7 not found"));
}
#[test]
fn trait_and_inherent_formatters_do_not_diverge() {
let err = SanthError::new("TEST-CONTRACT", "Boom")
.with_context("k", "v")
.with_location(ErrorLocation::new("f.toml").with_line(3))
.fix("Fix: do the thing.")
.build();
let via_trait = SanthErrorContract::actionable_message(&err);
let via_inherent = err.actionable_message();
assert_eq!(
via_trait, via_inherent,
"the trait default and the inherent formatter must produce identical output"
);
}
#[test]
fn contract_message_redacts_secret_in_context_value() {
let err = SanthError::new("TEST-RED", "Auth failed")
.with_context("authorization", "Bearer sk-abcdefghij0123456789XYZ")
.fix("Fix: rotate the token and retry.")
.build();
let msg = SanthErrorContract::actionable_message(&err);
assert!(
!msg.contains("sk-abcdefghij0123456789XYZ"),
"a secret in context must be redacted from the contract message: {msg}"
);
assert!(msg.contains("[REDACTED]"));
}
#[test]
fn contract_error_code_is_stable_per_variant() {
assert_eq!(
DomainError::NotFound("x".to_string()).error_code(),
"DOMAIN-E002"
);
assert_eq!(
DomainError::ShapeMismatch { want: 1, got: 2 }.error_code(),
"DOMAIN-E001"
);
}
#[test]
fn builder_and_built_mutators_render_identically() {
#[derive(Debug)]
struct Cause;
impl fmt::Display for Cause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "underlying io failure")
}
}
impl std::error::Error for Cause {}
let via_builder = SanthError::new("PARITY-E001", "Parity check")
.with_context("key", "value")
.with_source(Cause)
.with_location(ErrorLocation::new("src/lib.rs").with_line(42).with_column(7))
.fix("Fix: nothing, this is a parity probe.")
.build();
let via_built = SanthError::new("PARITY-E001", "Parity check")
.fix("Fix: nothing, this is a parity probe.")
.build()
.with_context("key", "value")
.with_source(Cause)
.with_location(ErrorLocation::new("src/lib.rs").with_line(42).with_column(7));
assert_eq!(
via_builder.actionable_message(),
via_built.actionable_message(),
"builder-side and built-side diagnostic mutators must be the same single implementation"
);
let msg = via_built.actionable_message();
assert!(msg.contains("Context:"), "context must render: {msg}");
assert!(msg.contains(" key: value"), "context entry must render: {msg}");
assert!(msg.contains("Location: src/lib.rs:42:7"), "location must render: {msg}");
assert!(msg.contains("Caused by:"), "source chain must render: {msg}");
assert!(msg.contains("underlying io failure"), "source msg must render: {msg}");
}
#[test]
fn contract_fix_hint_without_prefix_gets_normalised() {
#[derive(Debug)]
struct CustomContractError;
impl fmt::Display for CustomContractError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Custom failure")
}
}
impl std::error::Error for CustomContractError {}
impl SanthErrorContract for CustomContractError {
fn error_code(&self) -> &'static str {
"CUSTOM-E001"
}
fn fix_hint(&self) -> Cow<'_, str> {
Cow::Borrowed("Verify network connectivity and retry.")
}
}
let err = CustomContractError;
let msg = err.actionable_message();
assert!(
msg.contains("Fix: Verify network connectivity and retry."),
"actionable message must normalise missing 'Fix: ' prefix: {msg}"
);
}