use std::error::Error as StdError;
use bao::browser::BrowserError;
use bao::cdp_client::{CdpError, ConnectError};
use bao_crypto::CryptoError;
use bun_sm::JsError;
#[allow(unused_imports)]
use bun_sm::ErrorCode;
use bun_transpiler::TranspileError;
fn golden() -> serde_json::Value {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/errors.json");
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read golden {}: {e}", path.display()));
serde_json::from_str(&raw).expect("golden errors.json must be valid JSON")
}
fn variant_name(err: &dyn StdError) -> String {
let dbg = format!("{err:?}");
let name: String = dbg
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect();
assert!(!name.is_empty(), "Debug form must start with a variant name: {dbg}");
name
}
fn browser_cases() -> Vec<(Box<dyn StdError>, &'static str, &'static str)> {
vec![
(Box::new(BrowserError::Init("x".into())), "Init", "browser init error: x"),
(Box::new(BrowserError::Navigation("x".into())), "Navigation", "navigation error: x"),
(Box::new(BrowserError::Rendering("x".into())), "Rendering", "rendering error: x"),
(Box::new(BrowserError::JavaScript("x".into())), "JavaScript", "javascript error: x"),
(Box::new(BrowserError::CDP("x".into())), "CDP", "cdp error: x"),
]
}
fn connect_cases() -> Vec<(Box<dyn StdError>, &'static str, &'static str)> {
vec![
(Box::new(ConnectError::InvalidUrl), "InvalidUrl", "invalid URL (empty or missing scheme)"),
(Box::new(ConnectError::InvalidScheme("wsx".into())), "InvalidScheme", "invalid URL scheme:"),
(Box::new(ConnectError::LaunchError("x".into())), "LaunchError", "browser launch failed: x"),
(Box::new(ConnectError::ConnectionFailed("x".into())), "ConnectionFailed", "connection failed: x"),
(Box::new(ConnectError::Timeout("x".into())), "Timeout", "connect timeout: x"),
(Box::new(ConnectError::Io(std::io::Error::other("root-cause"))), "Io", "connection I/O error: root-cause"),
]
}
fn cdp_cases() -> Vec<(Box<dyn StdError>, &'static str, &'static str)> {
vec![
(Box::new(CdpError::ProtocolError("x".into())), "ProtocolError", "CDP protocol error: x"),
(Box::new(CdpError::JsonError(serde_json::from_str::<serde_json::Value>("{bad").unwrap_err())), "JsonError", "JSON error:"),
(Box::new(CdpError::IoError(std::io::Error::other("boom"))), "IoError", "I/O error: boom"),
(Box::new(CdpError::ConnectionClosed), "ConnectionClosed", "connection closed"),
(Box::new(CdpError::Timeout("x".into())), "Timeout", "timeout: x"),
(Box::new(CdpError::TransportError("x".into())), "TransportError", "transport error: x"),
(Box::new(CdpError::HandshakeError("x".into())), "HandshakeError", "WebSocket handshake error: x"),
(Box::new(CdpError::Connect(ConnectError::InvalidUrl)), "Connect", "connect error: invalid URL (empty or missing scheme)"),
]
}
fn crypto_cases() -> Vec<(Box<dyn StdError>, &'static str, &'static str)> {
vec![
(Box::new(CryptoError::InvalidKeyLength { expected: 1, got: 2 }), "InvalidKeyLength", "Invalid key length:"),
(Box::new(CryptoError::InvalidNonceLength { expected: 1, got: 2 }), "InvalidNonceLength", "Invalid nonce length:"),
(Box::new(CryptoError::InvalidKey("x".into())), "InvalidKey", "Invalid key: x"),
(Box::new(CryptoError::InvalidCurve("x".into())), "InvalidCurve", "Invalid curve: x"),
(Box::new(CryptoError::InvalidCertificate("x".into())), "InvalidCertificate", "Invalid certificate: x"),
(Box::new(CryptoError::InvalidSignature("x".into())), "InvalidSignature", "Invalid signature: x"),
(Box::new(CryptoError::InvalidLength("x".into())), "InvalidLength", "Invalid length: x"),
(Box::new(CryptoError::InvalidFormat("x".into())), "InvalidFormat", "Invalid format: x"),
(Box::new(CryptoError::UnsupportedAlgorithm("x".into())), "UnsupportedAlgorithm", "Unsupported algorithm: x"),
(Box::new(CryptoError::EncryptionFailed("x".into())), "EncryptionFailed", "Encryption failed: x"),
(Box::new(CryptoError::DecryptionFailed("x".into())), "DecryptionFailed", "Decryption failed: x"),
(Box::new(CryptoError::SignFailed("x".into())), "SignFailed", "Signing failed: x"),
(Box::new(CryptoError::VerifyFailed("x".into())), "VerifyFailed", "Verification failed: x"),
(Box::new(CryptoError::EncodingFailed("x".into())), "EncodingFailed", "Encoding failed: x"),
(Box::new(CryptoError::DecodingFailed("x".into())), "DecodingFailed", "Decoding failed: x"),
(Box::new(CryptoError::CertificateError("x".into())), "CertificateError", "Certificate error: x"),
(Box::new(CryptoError::KdfError("x".into())), "KdfError", "KDF error: x"),
(Box::new(CryptoError::KeyExchangeError("x".into())), "KeyExchangeError", "Key exchange error: x"),
(Box::new(CryptoError::KeyPairError("x".into())), "KeyPairError", "Key pair generation error: x"),
(Box::new(CryptoError::KeyGenerationFailed("x".into())), "KeyGenerationFailed", "Key generation failed: x"),
(Box::new(CryptoError::SharedSecretFailed("x".into())), "SharedSecretFailed", "Shared secret computation failed: x"),
(Box::new(CryptoError::RandomFailed("x".into())), "RandomFailed", "Random generation failed: x"),
(Box::new(CryptoError::OperationFailed("x".into())), "OperationFailed", "Operation failed: x"),
]
}
#[test]
fn error_golden_variant_and_display_lock() {
let g = golden();
let groups: Vec<(&str, Vec<(Box<dyn StdError>, &'static str, &'static str)>)> = vec![
("BrowserError", browser_cases()),
("ConnectError", connect_cases()),
("CdpError", cdp_cases()),
("CryptoError", crypto_cases()),
];
for (type_name, cases) in &groups {
let golden_variants: Vec<String> = g[type_name]["variants"]
.as_array()
.expect("golden variants array")
.iter()
.map(|v| v.as_str().expect("variant string").to_string())
.collect();
for (err, variant, prefix) in cases {
assert!(
golden_variants.iter().any(|gv| gv == variant),
"{type_name}::{variant} is constructed but missing from golden variants {:?}",
golden_variants
);
let disp = err.to_string();
assert!(
disp.starts_with(prefix),
"{type_name}::{variant} Display drifted: got {disp:?}, expected prefix {prefix:?}"
);
assert_eq!(
&variant_name(err.as_ref()), variant,
"{type_name} Debug head must equal the variant name"
);
}
for gv in &golden_variants {
assert!(
cases.iter().any(|(_, v, _)| v == gv),
"golden variant {type_name}::{gv} has no constructed case in this test"
);
}
assert_eq!(
cases.len(),
golden_variants.len(),
"{type_name}: constructed case count must equal golden variant count"
);
}
}
#[test]
fn error_golden_struct_shapes_lock() {
let g = golden();
let te = TranspileError { message: "boom".into() };
assert_eq!(te.to_string(), "transpile_ts error: boom");
assert_eq!(g["TranspileError"]["display"].as_str(), Some("transpile_ts error: "));
let je = JsError {
message: "oops".into(),
filename: "app.js".into(),
line: 3,
column: 7,
stack: Some("at f".into()),
};
let disp = je.to_string();
assert!(disp.starts_with("app.js:3:7: oops"), "JsError Display drifted: {disp:?}");
assert!(disp.contains("at f"), "JsError must carry the stack block");
let no_stack = JsError {
message: "m".into(),
filename: "f.js".into(),
line: 1,
column: 1,
stack: None,
};
assert_eq!(no_stack.to_string(), "f.js:1:1: m");
let disc = g["ErrorCode"]["discriminants"].as_object().expect("discriminants");
let pairs = [
("NoError", 0u8),
("TypeError", 1),
("RangeError", 2),
("SyntaxError", 3),
("ReferenceError", 4),
("StackOverflow", 5),
("OutOfMemory", 6),
("URIError", 7),
("EvalError", 8),
("InternalError", 9),
("GenericError", 10),
];
for (name, value) in pairs {
assert_eq!(
disc.get(name).and_then(|v| v.as_u64()),
Some(value as u64),
"ErrorCode::{name} discriminant drifted"
);
let _ = name;
}
}
#[test]
fn error_taxonomy_error_trait_and_source_chain() {
let browser: Box<dyn StdError> = Box::new(BrowserError::Init("x".into()));
let connect: Box<dyn StdError> = Box::new(ConnectError::InvalidUrl);
let cdp: Box<dyn StdError> = Box::new(CdpError::ConnectionClosed);
let crypto: Box<dyn StdError> = Box::new(CryptoError::InvalidKey("x".into()));
let transpile: Box<dyn StdError> = Box::new(TranspileError { message: "x".into() });
let js: Box<dyn StdError> = Box::new(JsError {
message: "x".into(),
filename: "f".into(),
line: 1,
column: 1,
stack: None,
});
for (name, e) in [
("BrowserError", &*browser),
("ConnectError", &*connect),
("CdpError", &*cdp),
("CryptoError", &*crypto),
("TranspileError", &*transpile),
("JsError", &*js),
] {
assert!(!e.to_string().is_empty(), "{name} must render non-empty Display");
}
let io_err = CdpError::IoError(std::io::Error::other("root-cause"));
let src = io_err
.source()
.expect("CdpError::IoError must expose its io::Error source (W29 w22d-2)");
assert!(
src.to_string().contains("root-cause"),
"the exposed source must carry the root cause: {src}"
);
let conn: ConnectError = std::io::Error::other("root-cause").into();
match &conn {
ConnectError::Io(err) => assert!(
err.to_string().contains("root-cause"),
"the typed io source must survive: {err}"
),
other => panic!("From<io::Error> must build ConnectError::Io (W29), got {other:?}"),
}
assert!(
conn.source().is_some(),
"ConnectError::Io must expose its source"
);
let nested: CdpError = ConnectError::Io(std::io::Error::other("root-cause")).into();
let level1 = nested
.source()
.expect("CdpError::Connect must expose the nested ConnectError");
assert!(
format!("{level1:?}").contains("Io"),
"level 1 must be the ConnectError (phase preserved): {level1:?}"
);
let level2 = level1
.source()
.expect("the nested ConnectError::Io must expose its io source");
assert!(
level2.to_string().contains("root-cause"),
"level 2 must carry the root cause: {level2}"
);
let json_err: CdpError = serde_json::from_str::<serde_json::Value>("{bad").unwrap_err().into();
assert!(
json_err.source().is_some(),
"CdpError::JsonError must expose its serde_json source (W29 w22d-3)"
);
assert!(CdpError::ConnectionClosed.source().is_none());
assert!(BrowserError::Init("x".into()).source().is_none());
}