use hyperdb_mcp::error::{is_connection_lost, ErrorCode, McpError};
#[test]
fn error_code_to_string() {
let err = McpError::new(
ErrorCode::HyperdNotFound,
"hyperd not found at /usr/local/bin/hyperd",
);
assert_eq!(err.code, ErrorCode::HyperdNotFound);
assert!(err.message.contains("hyperd not found"));
assert!(err.suggestion.is_some());
}
#[test]
fn error_to_json() {
let err = McpError::new(ErrorCode::SqlError, "syntax error at position 42");
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["code"], "SQL_ERROR");
assert_eq!(json["message"], "syntax error at position 42");
}
#[test]
fn classifies_transport_disappearance_as_connection_lost() {
for msg in [
"Broken pipe (os error 32)",
"Connection reset by peer",
"Connection refused (os error 61)",
"connection closed by server",
"unexpected EOF while reading frame header",
"unexpected end of file during read",
"server unexpectedly closed the connection",
"socket is not connected",
] {
assert!(
is_connection_lost(msg),
"expected transport error to be classified as connection-lost: {msg:?}",
);
}
}
#[test]
fn classifies_desynchronized_wire_as_connection_lost() {
let desync_msg = "connection is desynchronized from the server and \
cannot be reused; discard it and open a new one";
assert!(
is_connection_lost(desync_msg),
"desynchronized wire must be classified as connection-lost",
);
assert!(is_connection_lost(
"ERROR: connection has been desynchronized and cannot continue",
));
assert!(is_connection_lost("DESYNCHRONIZED: drop this conn"));
}
#[test]
fn does_not_classify_unrelated_errors_as_connection_lost() {
for msg in [
"syntax error at or near \"SELECT\"",
"table \"foo\" does not exist",
"permission denied for file /tmp/data.csv",
"No such file or directory",
"disk full",
] {
assert!(
!is_connection_lost(msg),
"unrelated error must NOT be classified as connection-lost: {msg:?}",
);
}
}
#[test]
fn maps_22003_to_schema_mismatch_with_override_suggestion() {
let upstream =
hyperdb_api::Error::server(Some("22003".to_string()), "numeric overflow", None, None);
let mcp: McpError = upstream.into();
assert_eq!(mcp.code, ErrorCode::SchemaMismatch);
let suggestion = mcp.suggestion.expect("22003 must have a suggestion");
let lower = suggestion.to_lowercase();
assert!(
lower.contains("schema") && lower.contains("override"),
"suggestion should mention schema override; got: {suggestion}",
);
assert!(
lower.contains("bigint") || lower.contains("numeric"),
"suggestion should hint at BIGINT/NUMERIC widenings; got: {suggestion}",
);
}
#[test]
fn maps_out_of_range_phrase_to_schema_mismatch() {
let upstream =
hyperdb_api::Error::server(None, "value out of range for type integer", None, None);
let mcp: McpError = upstream.into();
assert_eq!(mcp.code, ErrorCode::SchemaMismatch);
assert!(mcp.suggestion.is_some());
}
#[test]
fn maps_22p02_to_schema_mismatch_with_text_suggestion() {
let upstream = hyperdb_api::Error::server(
Some("22P02".to_string()),
"invalid input syntax for type date",
None,
None,
);
let mcp: McpError = upstream.into();
assert_eq!(mcp.code, ErrorCode::SchemaMismatch);
let suggestion = mcp.suggestion.expect("22P02 must have a suggestion");
assert!(
suggestion.to_uppercase().contains("TEXT"),
"suggestion should recommend overriding to TEXT; got: {suggestion}",
);
}
#[test]
fn connection_lost_suggestion_mentions_both_triggers() {
let err = McpError::new(ErrorCode::ConnectionLost, "connection is desynchronized");
let suggestion = err.suggestion.expect("ConnectionLost has a suggestion");
let lower = suggestion.to_lowercase();
assert!(
lower.contains("lost") || lower.contains("desync") || lower.contains("sync"),
"suggestion should hint at both triggers; got: {suggestion}",
);
assert!(
lower.contains("retry") || lower.contains("reconnect") || lower.contains("automatically"),
"suggestion should tell the caller the server will recover; got: {suggestion}",
);
}
#[test]
fn maps_invalid_name_to_invalid_argument() {
for upstream in [
hyperdb_api::Error::invalid_name(
"KV key contains an invalid byte 0x20; allowed: A-Z a-z 0-9 _ . -",
),
hyperdb_api::Error::invalid_name("KV store name must not be empty"),
hyperdb_api::Error::invalid_name("KV key exceeds 512-byte limit (630 bytes)"),
] {
let original = upstream.to_string();
let mcp: McpError = upstream.into();
assert_eq!(
mcp.code,
ErrorCode::InvalidArgument,
"invalid name must be InvalidArgument, not InternalError: {original}",
);
assert!(
mcp.message.contains("invalid name"),
"message should carry the validation detail; got: {}",
mcp.message,
);
assert!(
mcp.suggestion.is_some(),
"InvalidArgument carries a self-correction suggestion",
);
}
}
#[test]
fn maps_invalid_table_definition_to_invalid_argument() {
let def: McpError =
hyperdb_api::Error::invalid_table_definition("table must have at least one column").into();
assert_eq!(def.code, ErrorCode::InvalidArgument);
}
#[test]
fn keeps_invalid_operation_as_internal_error() {
let op: McpError = hyperdb_api::Error::invalid_operation("inserter already finalized").into();
assert_eq!(op.code, ErrorCode::InternalError);
}