use crate::cli::session_notify::post_jsonrpc;
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn closed_port() -> u16 {
let l = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
let port = l.local_addr().expect("local addr").port();
drop(l);
port
}
#[tokio::test]
async fn dead_listener_classifies_as_connect() {
let port = closed_port().await;
let url = format!("http://127.0.0.1:{port}/a2a/v1");
let err = post_jsonrpc(
&url,
None,
&json!({"jsonrpc": "2.0", "id": 1, "method": "x"}),
)
.await
.expect_err("nothing is listening: the send must fail");
assert!(
err.contains("connect:"),
"must name the connect phase, got: {err}"
);
assert!(
!err.contains("error sending request for url"),
"the bare opaque Display must no longer be the whole detail: {err}"
);
assert!(
err.contains("(attempt 3/3)"),
"the journal must show the LAST attempt failed, got: {err}"
);
}
#[tokio::test]
async fn http_502_without_jsonrpc_names_the_response_failure() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind canned server");
let port = listener.local_addr().expect("local addr").port();
let server = tokio::spawn(async move {
for _ in 0..4 {
let Ok((mut sock, _)) = listener.accept().await else {
break;
};
let mut buf = [0u8; 2048];
let _ = sock.read(&mut buf).await;
let _ = sock
.write_all(
b"HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
)
.await;
}
});
let url = format!("http://127.0.0.1:{port}/a2a/v1");
let err = post_jsonrpc(
&url,
None,
&json!({"jsonrpc": "2.0", "id": 1, "method": "x"}),
)
.await
.expect_err("a 502 with no JSON-RPC body carries no decision: must fail");
assert!(
err.contains("without a JSON-RPC body"),
"must name the response-shape failure, got: {err}"
);
assert!(
err.contains("502"),
"must carry the status code, got: {err}"
);
assert!(
err.contains("(attempt 3/3)"),
"the journal must show the LAST attempt failed, got: {err}"
);
server.abort();
}