use chrono::{TimeZone, Utc};
use serde_json::json;
use weavegraph::channels::errors::*;
use weavegraph::channels::{Channel, ErrorsChannel};
use weavegraph::types::ChannelType;
#[test]
fn ladder_error_msg_and_chain() {
let base = WeaveError::msg("root cause").with_details(json!({"k":"v"}));
let wrapped = WeaveError::msg("top").with_cause(base.clone());
assert_eq!(base.message, "root cause");
assert_eq!(wrapped.message, "top");
assert!(wrapped.cause.is_some());
assert_eq!(wrapped.cause.as_ref().unwrap().message, base.message);
assert_eq!(base.details, json!({"k":"v"}));
}
#[test]
fn ladder_error_serde_roundtrip() {
let err = WeaveError::msg("boom")
.with_details(json!({"code": 500}))
.with_cause(WeaveError::msg("inner"));
let ser = serde_json::to_string(&err).expect("serialize");
let de: WeaveError = serde_json::from_str(&ser).expect("deserialize");
assert_eq!(de, err);
}
#[test]
fn error_scope_enum_variants_serde() {
let node = ErrorScope::Node {
kind: "Custom:Parser".into(),
step: 42,
};
let ser_node = serde_json::to_value(&node).unwrap();
assert_eq!(ser_node["scope"], "node");
assert_eq!(ser_node["kind"], "Custom:Parser");
assert_eq!(ser_node["step"], 42);
let sch = ErrorScope::Scheduler { step: 10 };
let ser_sch = serde_json::to_value(&sch).unwrap();
assert_eq!(ser_sch["scope"], "scheduler");
let run = ErrorScope::Runner {
session: "abc".into(),
step: 7,
};
let ser_run = serde_json::to_value(&run).unwrap();
assert_eq!(ser_run["scope"], "runner");
let app = ErrorScope::App;
let ser_app = serde_json::to_value(&app).unwrap();
assert_eq!(ser_app["scope"], "app");
assert_eq!(
serde_json::from_value::<ErrorScope>(ser_node).unwrap(),
node
);
assert_eq!(serde_json::from_value::<ErrorScope>(ser_sch).unwrap(), sch);
assert_eq!(serde_json::from_value::<ErrorScope>(ser_run).unwrap(), run);
assert_eq!(serde_json::from_value::<ErrorScope>(ser_app).unwrap(), app);
}
#[test]
fn error_event_defaults_and_roundtrip() {
let when = Utc.with_ymd_and_hms(2024, 1, 2, 3, 4, 5).unwrap();
let ev = ErrorEvent {
when,
scope: ErrorScope::App,
error: WeaveError::msg("oops"),
tags: vec!["t1".into(), "t2".into()],
context: json!({"info": true}),
};
let ser = serde_json::to_string(&ev).unwrap();
let de: ErrorEvent = serde_json::from_str(&ser).unwrap();
assert_eq!(de, ev);
}
#[test]
fn error_event_defaults_are_empty_when_missing() {
let when = Utc.with_ymd_and_hms(2024, 5, 6, 7, 8, 9).unwrap();
let v = json!({
"when": when,
"scope": {"scope": "app"},
"error": {"message":"x"}
});
let de: ErrorEvent = serde_json::from_value(v).unwrap();
assert!(de.tags.is_empty());
assert!(de.context.is_null());
}
#[test]
fn error_event_node_constructor() {
let err = ErrorEvent::node("Parser", 42, WeaveError::msg("parse failed"));
assert!(matches!(err.scope, ErrorScope::Node { .. }));
if let ErrorScope::Node { kind, step } = err.scope {
assert_eq!(kind, "Parser");
assert_eq!(step, 42);
}
assert_eq!(err.error.message, "parse failed");
assert!(err.tags.is_empty());
assert!(err.context.is_null());
}
#[test]
fn error_event_scheduler_constructor() {
let err = ErrorEvent::scheduler(10, WeaveError::msg("scheduling conflict"));
assert!(matches!(err.scope, ErrorScope::Scheduler { .. }));
if let ErrorScope::Scheduler { step } = err.scope {
assert_eq!(step, 10);
}
assert_eq!(err.error.message, "scheduling conflict");
assert!(err.tags.is_empty());
assert!(err.context.is_null());
}
#[test]
fn error_event_runner_constructor() {
let err = ErrorEvent::runner("session-abc", 99, WeaveError::msg("runtime error"));
assert!(matches!(err.scope, ErrorScope::Runner { .. }));
if let ErrorScope::Runner { session, step } = err.scope {
assert_eq!(session, "session-abc");
assert_eq!(step, 99);
}
assert_eq!(err.error.message, "runtime error");
assert!(err.tags.is_empty());
assert!(err.context.is_null());
}
#[test]
fn error_event_app_constructor() {
let err = ErrorEvent::app(WeaveError::msg("startup failed"));
assert!(matches!(err.scope, ErrorScope::App));
assert_eq!(err.error.message, "startup failed");
assert!(err.tags.is_empty());
assert!(err.context.is_null());
}
#[test]
fn error_event_with_tag_builder() {
let err = ErrorEvent::node("Validator", 1, WeaveError::msg("invalid")).with_tag("validation");
assert_eq!(err.tags, vec!["validation"]);
}
#[test]
fn error_event_with_multiple_tags_chained() {
let err = ErrorEvent::scheduler(5, WeaveError::msg("error"))
.with_tag("critical")
.with_tag("retry");
assert_eq!(err.tags, vec!["critical", "retry"]);
}
#[test]
fn error_event_with_tags_builder() {
let err = ErrorEvent::runner("sess-1", 3, WeaveError::msg("failed"))
.with_tags(vec!["urgent".to_string(), "logged".to_string()]);
assert_eq!(err.tags, vec!["urgent", "logged"]);
}
#[test]
fn error_event_with_context_builder() {
let err = ErrorEvent::app(WeaveError::msg("config error"))
.with_context(json!({"config_file": "/etc/app.conf", "line": 42}));
assert_eq!(err.context["config_file"], "/etc/app.conf");
assert_eq!(err.context["line"], 42);
}
#[test]
fn error_event_full_builder_chain() {
let err = ErrorEvent::node(
"Analyzer",
7,
WeaveError::msg("analysis failed")
.with_cause(WeaveError::msg("missing data"))
.with_details(json!({"field": "input"})),
)
.with_tag("retryable")
.with_tag("logged")
.with_context(json!({"attempt": 3, "max_attempts": 5}));
if let ErrorScope::Node { kind, step } = err.scope {
assert_eq!(kind, "Analyzer");
assert_eq!(step, 7);
} else {
panic!("Expected Node scope");
}
assert_eq!(err.error.message, "analysis failed");
assert!(err.error.cause.is_some());
assert_eq!(err.error.cause.as_ref().unwrap().message, "missing data");
assert_eq!(err.error.details["field"], "input");
assert_eq!(err.tags, vec!["retryable", "logged"]);
assert_eq!(err.context["attempt"], 3);
assert_eq!(err.context["max_attempts"], 5);
}
#[test]
fn error_event_constructors_serialize_correctly() {
let manual = ErrorEvent {
when: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
scope: ErrorScope::Node {
kind: "Test".to_string(),
step: 1,
},
error: WeaveError::msg("test"),
tags: vec!["tag1".to_string()],
context: json!({"key": "value"}),
};
let constructed = ErrorEvent::node("Test", 1, WeaveError::msg("test"))
.with_tag("tag1")
.with_context(json!({"key": "value"}));
let manual_json = serde_json::to_value(&manual).unwrap();
let constructed_json = serde_json::to_value(&constructed).unwrap();
assert_eq!(manual_json["scope"], constructed_json["scope"]);
assert_eq!(manual_json["error"], constructed_json["error"]);
assert_eq!(manual_json["tags"], constructed_json["tags"]);
assert_eq!(manual_json["context"], constructed_json["context"]);
}
#[test]
fn error_event_string_into_conversions() {
let from_str = ErrorEvent::node("literal", 1, WeaveError::msg("test"));
let from_string = ErrorEvent::node(String::from("owned"), 1, WeaveError::msg("test"));
if let ErrorScope::Node { kind, .. } = from_str.scope {
assert_eq!(kind, "literal");
}
if let ErrorScope::Node { kind, .. } = from_string.scope {
assert_eq!(kind, "owned");
}
let runner_str = ErrorEvent::runner("session", 1, WeaveError::msg("test"));
let runner_string = ErrorEvent::runner(String::from("session_id"), 1, WeaveError::msg("test"));
if let ErrorScope::Runner { session, .. } = runner_str.scope {
assert_eq!(session, "session");
}
if let ErrorScope::Runner { session, .. } = runner_string.scope {
assert_eq!(session, "session_id");
}
}
#[test]
fn test_error_event_serialization_all_scopes() {
let when = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
let mut node_event = ErrorEvent::node("TestNode", 42, WeaveError::msg("node error"))
.with_tag("test")
.with_context(json!({"node_id": 42}));
node_event.when = when;
let node_json = serde_json::to_value(&node_event).unwrap();
assert_eq!(node_json["scope"]["scope"], "node");
assert_eq!(node_json["scope"]["kind"], "TestNode");
assert_eq!(node_json["scope"]["step"], 42);
assert_eq!(node_json["error"]["message"], "node error");
assert_eq!(node_json["tags"], json!(["test"]));
assert_eq!(node_json["context"]["node_id"], 42);
let node_deserialized: ErrorEvent = serde_json::from_value(node_json).unwrap();
assert_eq!(node_deserialized.scope, node_event.scope);
assert_eq!(node_deserialized.error, node_event.error);
assert_eq!(node_deserialized.tags, node_event.tags);
assert_eq!(node_deserialized.context, node_event.context);
let mut scheduler_event = ErrorEvent::scheduler(10, WeaveError::msg("scheduler error"));
scheduler_event.when = when;
let scheduler_json = serde_json::to_value(&scheduler_event).unwrap();
assert_eq!(scheduler_json["scope"]["scope"], "scheduler");
assert_eq!(scheduler_json["scope"]["step"], 10);
let scheduler_deserialized: ErrorEvent = serde_json::from_value(scheduler_json).unwrap();
assert_eq!(scheduler_deserialized.scope, scheduler_event.scope);
let mut runner_event = ErrorEvent::runner("session-123", 5, WeaveError::msg("runner error"));
runner_event.when = when;
let runner_json = serde_json::to_value(&runner_event).unwrap();
assert_eq!(runner_json["scope"]["scope"], "runner");
assert_eq!(runner_json["scope"]["session"], "session-123");
assert_eq!(runner_json["scope"]["step"], 5);
let runner_deserialized: ErrorEvent = serde_json::from_value(runner_json).unwrap();
assert_eq!(runner_deserialized.scope, runner_event.scope);
let mut app_event = ErrorEvent::app(WeaveError::msg("app error"));
app_event.when = when;
let app_json = serde_json::to_value(&app_event).unwrap();
assert_eq!(app_json["scope"]["scope"], "app");
let app_deserialized: ErrorEvent = serde_json::from_value(app_json).unwrap();
assert_eq!(app_deserialized.scope, app_event.scope);
}
#[test]
fn test_ladder_error_nested_serialization() {
let simple_error = WeaveError::msg("simple error");
let simple_json = serde_json::to_value(&simple_error).unwrap();
assert_eq!(simple_json["message"], "simple error");
assert!(simple_json["cause"].is_null());
assert!(simple_json["details"].is_null());
let error_with_details =
WeaveError::msg("error with details").with_details(json!({"code": 500, "retry": true}));
let details_json = serde_json::to_value(&error_with_details).unwrap();
assert_eq!(details_json["message"], "error with details");
assert_eq!(details_json["details"]["code"], 500);
assert_eq!(details_json["details"]["retry"], true);
let details_deserialized: WeaveError = serde_json::from_value(details_json).unwrap();
assert_eq!(details_deserialized, error_with_details);
let error_with_cause =
WeaveError::msg("outer error").with_cause(WeaveError::msg("inner error"));
let cause_json = serde_json::to_value(&error_with_cause).unwrap();
assert_eq!(cause_json["message"], "outer error");
assert_eq!(cause_json["cause"]["message"], "inner error");
assert!(cause_json["cause"]["cause"].is_null());
let cause_deserialized: WeaveError = serde_json::from_value(cause_json).unwrap();
assert_eq!(cause_deserialized, error_with_cause);
let deep_error = WeaveError::msg("level 1").with_cause(
WeaveError::msg("level 2")
.with_cause(WeaveError::msg("level 3").with_details(json!({"deep": true}))),
);
let deep_json = serde_json::to_value(&deep_error).unwrap();
assert_eq!(deep_json["message"], "level 1");
assert_eq!(deep_json["cause"]["message"], "level 2");
assert_eq!(deep_json["cause"]["cause"]["message"], "level 3");
assert_eq!(deep_json["cause"]["cause"]["details"]["deep"], true);
let deep_deserialized: WeaveError = serde_json::from_value(deep_json).unwrap();
assert_eq!(deep_deserialized, deep_error);
}
#[test]
fn test_error_event_full_serialization_roundtrip() {
let original = ErrorEvent::node(
"ComplexNode",
99,
WeaveError::msg("complex error")
.with_cause(WeaveError::msg("root cause"))
.with_details(json!({"severity": "high"})),
)
.with_tags(vec!["critical".to_string(), "network".to_string()])
.with_context(json!({
"user_id": 12345,
"request_id": "req-abc-123",
"endpoint": "/api/process",
"metadata": {
"version": "1.2.3",
"environment": "production"
}
}));
let json_str = serde_json::to_string(&original).unwrap();
let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(json_value["scope"]["scope"], "node");
assert_eq!(json_value["scope"]["kind"], "ComplexNode");
assert_eq!(json_value["scope"]["step"], 99);
assert_eq!(json_value["error"]["message"], "complex error");
assert_eq!(json_value["error"]["cause"]["message"], "root cause");
assert_eq!(json_value["error"]["details"]["severity"], "high");
assert_eq!(json_value["tags"], json!(["critical", "network"]));
assert_eq!(json_value["context"]["user_id"], 12345);
assert_eq!(json_value["context"]["request_id"], "req-abc-123");
assert_eq!(json_value["context"]["endpoint"], "/api/process");
assert_eq!(json_value["context"]["metadata"]["version"], "1.2.3");
assert_eq!(
json_value["context"]["metadata"]["environment"],
"production"
);
let deserialized: ErrorEvent = serde_json::from_str(&json_str).unwrap();
assert_eq!(deserialized.scope, original.scope);
assert_eq!(deserialized.error, original.error);
assert_eq!(deserialized.tags, original.tags);
assert_eq!(deserialized.context, original.context);
}
#[test]
fn test_error_event_schema_stability() {
let event = ErrorEvent::node("SchemaTest", 1, WeaveError::msg("test"))
.with_tag("regression")
.with_context(json!({"test": true}));
let json = serde_json::to_value(&event).unwrap();
assert!(json.get("when").is_some(), "Missing 'when' field");
assert!(json.get("scope").is_some(), "Missing 'scope' field");
assert!(json.get("error").is_some(), "Missing 'error' field");
assert!(json.get("tags").is_some(), "Missing 'tags' field");
assert!(json.get("context").is_some(), "Missing 'context' field");
let scope = &json["scope"];
assert!(
scope.get("scope").is_some(),
"Missing 'scope.scope' discriminator"
);
let error = &json["error"];
assert!(
error.get("message").is_some(),
"Missing 'error.message' field"
);
assert!(json["tags"].is_array(), "tags should be an array");
assert!(
json["context"].is_object() || json["context"].is_null(),
"context should be object or null"
);
}
#[test]
fn test_error_scope_variants_complete_coverage() {
let test_cases = vec![
(
ErrorScope::Node {
kind: "MyNode".to_string(),
step: 1,
},
json!({"scope": "node", "kind": "MyNode", "step": 1}),
),
(
ErrorScope::Scheduler { step: 5 },
json!({"scope": "scheduler", "step": 5}),
),
(
ErrorScope::Runner {
session: "sess-x".to_string(),
step: 10,
},
json!({"scope": "runner", "session": "sess-x", "step": 10}),
),
(ErrorScope::App, json!({"scope": "app"})),
];
for (scope, expected_json) in test_cases {
let serialized = serde_json::to_value(&scope).unwrap();
assert_eq!(
serialized, expected_json,
"Serialization mismatch for {:?}",
scope
);
let deserialized: ErrorScope = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized, scope, "Round-trip failed for {:?}", scope);
}
}
#[test]
fn pretty_print_renders_usefully() {
let when = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let mut event = ErrorEvent::runner(
"sess-1",
3,
WeaveError::msg("failed").with_cause(WeaveError::msg("io")),
)
.with_tag("urgent")
.with_context(json!({"path":"/tmp/x"}));
event.when = when;
let events = vec![event];
let out = pretty_print(&events);
assert!(out.contains("failed"));
assert!(out.contains("cause: io"));
assert!(out.contains("Runner"));
assert!(out.contains("sess-1"));
assert!(out.contains("/tmp/x"));
}
#[test]
fn errors_channel_basics() {
let mut ch = ErrorsChannel::default();
assert_eq!(ch.get_channel_type(), ChannelType::Error);
assert!(ch.persistent());
assert_eq!(ch.version(), 1);
assert_eq!(ch.len(), 0);
assert!(ch.is_empty());
let when = Utc::now();
let err1 = ErrorEvent::scheduler(1, WeaveError::msg("first"));
let mut err1_with_time = err1;
err1_with_time.when = when;
ch.get_mut().push(err1_with_time);
let err2 = ErrorEvent::node("Start", 2, WeaveError::msg("second"))
.with_tag("retryable")
.with_context(json!({"try":2}));
let mut err2_with_time = err2;
err2_with_time.when = when;
ch.get_mut().push(err2_with_time);
assert_eq!(ch.len(), 2);
assert!(!ch.is_empty());
let snap = ch.snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].error.message, "first");
assert_eq!(snap[1].tags, vec!["retryable"]);
ch.set_version(5);
assert_eq!(ch.version(), 5);
}
#[test]
fn errors_channel_new_constructor() {
let when = Utc::now();
let mut e = ErrorEvent::app(WeaveError::msg("boom"));
e.when = when;
let ch = ErrorsChannel::new(vec![e.clone()], 7);
assert_eq!(ch.version(), 7);
assert_eq!(ch.snapshot(), vec![e]);
}
#[test]
fn optional_cli_pretty_demo() {
let when = Utc.with_ymd_and_hms(2024, 2, 2, 2, 2, 2).unwrap();
let mut event = ErrorEvent::app(WeaveError::msg("display"))
.with_tag("cli")
.with_context(json!({}));
event.when = when;
let events = vec![event];
let out = pretty_print(&events);
println!("\n=== Errors pretty showcase ===\n{}", out);
assert!(out.contains("display"));
}
#[test]
fn pretty_print_with_mode_colored_includes_ansi_codes() {
use weavegraph::telemetry::FormatterMode;
let event =
ErrorEvent::node("parser", 1, WeaveError::msg("Parse failed")).with_tag("validation");
let events = vec![event];
let output = pretty_print_with_mode(&events, FormatterMode::Colored);
assert!(
output.contains('\x1b'),
"Colored mode should include ANSI escape codes"
);
assert!(
output.contains("Parse failed"),
"Should include error message"
);
assert!(output.contains("validation"), "Should include tags");
}
#[test]
fn pretty_print_with_mode_plain_excludes_ansi_codes() {
use weavegraph::telemetry::FormatterMode;
let nested_error =
WeaveError::msg("Top level error").with_cause(WeaveError::msg("Nested cause"));
let event = ErrorEvent::scheduler(5, nested_error)
.with_tag("critical")
.with_context(json!({"attempt": 3}));
let events = vec![event];
let output = pretty_print_with_mode(&events, FormatterMode::Plain);
assert!(
!output.contains('\x1b'),
"Plain mode should not include ANSI escape codes"
);
assert!(
output.contains("Top level error"),
"Should include root error"
);
assert!(
output.contains("Nested cause"),
"Should include nested cause"
);
assert!(output.contains("critical"), "Should include tags");
assert!(output.contains("attempt"), "Should include context");
}
#[test]
fn pretty_print_uses_auto_mode_by_default() {
use weavegraph::telemetry::FormatterMode;
let event = ErrorEvent::app(WeaveError::msg("Test error"));
let events = vec![event.clone()];
let auto_output = pretty_print(&events);
let explicit_auto_output = pretty_print_with_mode(&events, FormatterMode::Auto);
assert_eq!(
auto_output, explicit_auto_output,
"pretty_print should be equivalent to pretty_print_with_mode(Auto)"
);
}