use opencode_voice::bridge::events::{next_reconnect_delay, parse_sse_block, SseEvent};
#[test]
fn test_heartbeat_produces_no_event() {
let block = r#"data: {"type":"server.heartbeat","properties":{}}"#;
assert!(
parse_sse_block(block).is_none(),
"server.heartbeat should produce no event"
);
}
#[test]
fn test_malformed_json_is_skipped() {
let block = "data: this-is-not-json";
assert!(
parse_sse_block(block).is_none(),
"Malformed JSON should be skipped"
);
}
#[test]
fn test_empty_data_line_is_skipped() {
let block = "event: ping\nid: 1";
assert!(
parse_sse_block(block).is_none(),
"Block with no data: line should be skipped"
);
}
#[test]
fn test_blank_data_value_is_skipped() {
let block = "data: ";
assert!(
parse_sse_block(block).is_none(),
"Block with blank data value should be skipped"
);
}
#[test]
fn test_permission_asked_parses_correctly() {
let block = r#"data: {"type":"permission.asked","properties":{"id":"perm-001","session_id":"sess-1","permission":"bash","patterns":[],"metadata":{},"always":[],"tool":null}}"#;
let event = parse_sse_block(block).expect("permission.asked should produce an event");
match event {
SseEvent::PermissionAsked(req) => {
assert_eq!(req.id, "perm-001");
assert_eq!(req.permission, "bash");
}
other => panic!("Expected PermissionAsked, got {:?}", other),
}
}
#[test]
fn test_permission_replied_parses_correctly() {
let block = r#"data: {"type":"permission.replied","properties":{"session_id":"s1","request_id":"r1","reply":"once"}}"#;
let event = parse_sse_block(block).expect("permission.replied should produce an event");
match event {
SseEvent::PermissionReplied {
session_id,
request_id,
reply,
} => {
assert_eq!(session_id, "s1");
assert_eq!(request_id, "r1");
assert_eq!(reply, "once");
}
other => panic!("Expected PermissionReplied, got {:?}", other),
}
}
#[test]
fn test_question_asked_parses_correctly() {
let block = r#"data: {"type":"question.asked","properties":{"id":"q-42","session_id":"sess-2","questions":[{"question":"Continue?","header":"Confirm","options":[],"multiple":false,"custom":true}],"tool":null}}"#;
let event = parse_sse_block(block).expect("question.asked should produce an event");
match event {
SseEvent::QuestionAsked(req) => {
assert_eq!(req.id, "q-42");
assert_eq!(req.questions.len(), 1);
assert_eq!(req.questions[0].question, "Continue?");
}
other => panic!("Expected QuestionAsked, got {:?}", other),
}
}
#[test]
fn test_question_replied_parses_correctly() {
let block = r#"data: {"type":"question.replied","properties":{"session_id":"s2","request_id":"r2","answers":[["yes","no"],["maybe"]]}}"#;
let event = parse_sse_block(block).expect("question.replied should produce an event");
match event {
SseEvent::QuestionReplied {
session_id,
request_id,
answers,
} => {
assert_eq!(session_id, "s2");
assert_eq!(request_id, "r2");
assert_eq!(answers, vec![vec!["yes", "no"], vec!["maybe"]]);
}
other => panic!("Expected QuestionReplied, got {:?}", other),
}
}
#[test]
fn test_question_rejected_parses_correctly() {
let block =
r#"data: {"type":"question.rejected","properties":{"session_id":"s3","request_id":"r3"}}"#;
let event = parse_sse_block(block).expect("question.rejected should produce an event");
match event {
SseEvent::QuestionRejected {
session_id,
request_id,
} => {
assert_eq!(session_id, "s3");
assert_eq!(request_id, "r3");
}
other => panic!("Expected QuestionRejected, got {:?}", other),
}
}
#[test]
fn test_server_connected_parses_correctly() {
let block = r#"data: {"type":"server.connected","properties":{}}"#;
let event = parse_sse_block(block).expect("server.connected should produce an event");
assert!(
matches!(event, SseEvent::Connected),
"Expected Connected, got {:?}",
event
);
}
#[test]
fn test_unknown_event_type_is_ignored() {
let block = r#"data: {"type":"some.future.event","properties":{"foo":"bar"}}"#;
assert!(
parse_sse_block(block).is_none(),
"Unknown event type should be silently ignored"
);
}
#[test]
fn test_missing_type_field_is_ignored() {
let block = r#"data: {"properties":{"id":"x"}}"#;
assert!(
parse_sse_block(block).is_none(),
"JSON without type field should be ignored"
);
}
#[test]
fn test_multiline_sse_block_finds_data_line() {
let block = "event: message\nid: 99\ndata: {\"type\":\"server.connected\"}";
let event = parse_sse_block(block).expect("should find data: line in multi-line block");
assert!(matches!(event, SseEvent::Connected));
}
#[test]
fn test_backoff_doubles_and_caps_at_30s() {
let mut delay: u64 = 1;
let mut sequence = Vec::new();
for _ in 0..8 {
sequence.push(delay);
delay = next_reconnect_delay(delay);
}
assert_eq!(
sequence,
vec![1, 2, 4, 8, 16, 30, 30, 30],
"Backoff sequence should be 1→2→4→8→16→30→30→30"
);
}
#[test]
fn test_backoff_individual_steps() {
assert_eq!(next_reconnect_delay(1), 2, "1s → 2s");
assert_eq!(next_reconnect_delay(2), 4, "2s → 4s");
assert_eq!(next_reconnect_delay(4), 8, "4s → 8s");
assert_eq!(next_reconnect_delay(8), 16, "8s → 16s");
assert_eq!(next_reconnect_delay(16), 30, "16s → 30s (capped)");
assert_eq!(next_reconnect_delay(30), 30, "30s → 30s (stays capped)");
assert_eq!(next_reconnect_delay(100), 30, "100s → 30s (capped)");
}
#[test]
fn test_backoff_cap_is_exactly_30s() {
assert_eq!(next_reconnect_delay(15), 30);
assert_eq!(next_reconnect_delay(16), 30);
assert_eq!(next_reconnect_delay(30), 30);
}