use std::collections::BTreeMap;
use operonx::core::exceptions::{truncate, OpError, OperonError};
#[test]
fn truncate_short_text_unchanged() {
assert_eq!(truncate("Hello World", 200), "Hello World");
}
#[test]
fn truncate_exact_length_unchanged() {
let text: String = "x".repeat(200);
let result = truncate(&text, 200);
assert_eq!(result, text);
assert_eq!(result.len(), 200);
}
#[test]
fn truncate_long_text_gets_ellipsis() {
let text: String = "x".repeat(300);
let result = truncate(&text, 200);
assert_eq!(result, format!("{}...", "x".repeat(200)));
assert_eq!(result.len(), 203);
}
#[test]
fn truncate_custom_max_length_respected() {
assert_eq!(truncate("Hello World", 5), "Hello...");
}
#[test]
fn op_error_parser_basic_display_includes_tag_and_message() {
let err = OpError::Parser {
message: "Invalid JSON syntax".into(),
input_text: r#"{"broken": "#.into(),
format_type: "json".into(),
original_error: None,
};
let msg = err.to_string();
assert!(msg.contains("[PARSER]"), "want [PARSER] in: {msg}");
assert!(msg.contains("Invalid JSON syntax"), "want body in: {msg}");
assert!(msg.contains("format:"), "want format: in: {msg}");
assert!(msg.contains("json"), "want json in: {msg}");
}
#[test]
fn op_error_parser_with_original_error_renders_error_line() {
let err = OpError::Parser {
message: "Failed to parse JSON".into(),
input_text: r#"{"broken": "#.into(),
format_type: "json".into(),
original_error: Some("Expecting value: line 1 column 12 (char 11)".into()),
};
let msg = err.to_string();
assert!(
msg.contains("Error: Expecting value"),
"want Error: line in: {msg}"
);
}
#[test]
fn op_error_parser_long_input_truncated() {
let long_text: String = "x".repeat(500);
let err = OpError::Parser {
message: "Parse failed".into(),
input_text: long_text,
format_type: "json".into(),
original_error: None,
};
let msg = err.to_string();
assert!(msg.contains("..."), "want ... ellipsis in: {msg}");
}
#[test]
fn op_error_parser_field_access() {
let err = OpError::Parser {
message: "x".into(),
input_text: "input".into(),
format_type: "xml".into(),
original_error: None,
};
if let OpError::Parser {
input_text,
format_type,
..
} = &err
{
assert_eq!(input_text, "input");
assert_eq!(format_type, "xml");
} else {
panic!("expected Parser variant");
}
}
#[test]
fn op_error_code_display_includes_function_and_source() {
let mut inputs = BTreeMap::new();
inputs.insert("x".into(), "1".into());
inputs.insert("y".into(), "2".into());
let err = OpError::Code {
message: "divide by zero".into(),
function_name: "calculate_total".into(),
source: "def calculate_total(x, y): return x / y".into(),
inputs,
original_error: Some("ZeroDivisionError".into()),
};
let msg = err.to_string();
assert!(msg.contains("[CODE]"), "want [CODE] in: {msg}");
assert!(msg.contains("divide by zero"), "want body in: {msg}");
assert!(msg.contains("calculate_total"), "want fn name in: {msg}");
assert!(
msg.contains("Error: ZeroDivisionError"),
"want Error: line in: {msg}"
);
}
#[test]
fn op_error_code_long_source_truncated() {
let long_source: String = "x".repeat(500);
let err = OpError::Code {
message: "fail".into(),
function_name: "f".into(),
source: long_source,
inputs: BTreeMap::new(),
original_error: None,
};
let msg = err.to_string();
assert!(msg.contains("..."), "want truncation in: {msg}");
}
#[test]
fn op_error_branch_display_includes_condition_and_candidates() {
let mut inputs = BTreeMap::new();
inputs.insert("score".into(), "invalid".into());
let err = OpError::Branch {
message: "Condition evaluation failed".into(),
condition: "score >= 90".into(),
inputs,
candidates: vec!["excellent".into(), "good".into(), "fail".into()],
original_error: Some("TypeError: unorderable types: str() >= int()".into()),
};
let msg = err.to_string();
assert!(msg.contains("[BRANCH]"));
assert!(msg.contains("score >= 90"));
assert!(msg.contains("'excellent'") || msg.contains("excellent"));
assert!(msg.contains("Error: TypeError"));
}
#[test]
fn op_error_condition_compile_phase() {
let err = OpError::Condition {
message: "Failed to compile".into(),
condition: "counter >= ((".into(),
inputs: BTreeMap::new(),
iteration: None,
phase: "compile".into(),
original_error: Some("SyntaxError".into()),
};
let msg = err.to_string();
assert!(msg.contains("[WHILE]"));
assert!(msg.contains("phase:"));
assert!(msg.contains("compile"));
}
#[test]
fn op_error_condition_eval_phase_with_iteration() {
let err = OpError::Condition {
message: "Stop condition evaluation failed".into(),
condition: "counter >= 5".into(),
inputs: BTreeMap::new(),
iteration: Some(3),
phase: "eval".into(),
original_error: None,
};
let msg = err.to_string();
assert!(msg.contains("iteration: 3"));
}
#[test]
fn op_error_iteration_for_loop_index_format() {
let mut data = BTreeMap::new();
data.insert("item".into(), "value".into());
let err = OpError::Iteration {
message: "Iteration 2 failed".into(),
iteration_index: 2,
loop_data: data,
total_iterations: 10,
op_type: "for".into(),
original_error: Some("KeyError".into()),
};
let msg = err.to_string();
assert!(msg.contains("[FOR]"));
assert!(msg.contains("2/10"), "want iteration_index 2/10 in: {msg}");
}
#[test]
fn op_error_prompt_missing_vars_render() {
let err = OpError::Prompt {
message: "Missing template variable".into(),
template_type: "str".into(),
template: "Hello {name}, your order {order_id}".into(),
missing_vars: vec!["order_id".into()],
original_error: None,
};
let msg = err.to_string();
assert!(msg.contains("[PROMPT]"));
assert!(msg.contains("missing_vars"));
assert!(msg.contains("order_id"));
}
#[test]
fn op_error_embedding_includes_resource_and_count() {
let err = OpError::Embedding {
message: "Backend failed".into(),
resource: "bge-m3".into(),
text_count: 100,
original_error: Some("ConnectionError".into()),
};
let msg = err.to_string();
assert!(msg.contains("[EMBEDDING]"));
assert!(msg.contains("bge-m3"));
assert!(msg.contains("text_count: 100"));
}
#[test]
fn op_error_rerank_includes_resource_query_count() {
let err = OpError::Rerank {
message: "Invalid document type".into(),
resource: "bge-m3".into(),
query: "search query".into(),
document_count: 50,
original_error: Some("TypeError".into()),
};
let msg = err.to_string();
assert!(msg.contains("[RERANK]"));
assert!(msg.contains("bge-m3"));
assert!(msg.contains("search query"));
assert!(msg.contains("document_count: 50"));
}
#[test]
fn op_error_kind_discriminator() {
let cases: Vec<(OpError, &str)> = vec![
(OpError::parser_msg("x", "json"), "parser"),
(OpError::code_msg("x", "f"), "code"),
(OpError::branch_msg("x", "c"), "branch"),
(
OpError::Condition {
message: "x".into(),
condition: "c".into(),
inputs: BTreeMap::new(),
iteration: None,
phase: "eval".into(),
original_error: None,
},
"while",
),
(
OpError::Iteration {
message: "x".into(),
iteration_index: 0,
loop_data: BTreeMap::new(),
total_iterations: 1,
op_type: "for".into(),
original_error: None,
},
"for",
),
(OpError::prompt_msg("x"), "prompt"),
(OpError::embedding_msg("x", "r", 1), "embedding"),
(OpError::rerank_msg("x", "r", 1), "rerank"),
];
for (err, expected) in cases {
assert_eq!(err.kind(), expected, "kind() mismatch for {err:?}");
}
}
#[test]
fn op_error_pattern_match_via_matches_macro() {
let err = OpError::parser_msg("x", "json");
assert!(matches!(err, OpError::Parser { .. }));
assert!(!matches!(err, OpError::Code { .. }));
}
#[test]
fn operon_error_from_op_error_preserves_variant() {
let op_err = OpError::parser_msg("invalid", "json");
let err: OperonError = op_err.into();
assert!(matches!(err, OperonError::Op(OpError::Parser { .. })));
}
#[test]
fn operon_error_display_through_op_wrapper_shows_tag() {
let err: OperonError = OpError::parser_msg("invalid", "json").into();
let msg = err.to_string();
assert!(
msg.contains("[PARSER]"),
"want [PARSER] through OperonError: {msg}"
);
assert!(msg.contains("invalid"));
}