story-kit: merge 138_bug_no_heartbeat_to_detect_stale_websocket_connections

This commit is contained in:
Dave
2026-02-24 13:05:30 +00:00
parent 71e07041cf
commit 5226438b16
3 changed files with 163 additions and 4 deletions

View File

@@ -29,6 +29,9 @@ enum WsRequest {
request_id: String,
approved: bool,
},
/// Heartbeat ping from the client. The server responds with `Pong` so the
/// client can detect stale (half-closed) connections.
Ping,
}
#[derive(Serialize)]
@@ -91,6 +94,9 @@ enum WsResponse {
status: String,
message: String,
},
/// Heartbeat response to a client `Ping`. Lets the client confirm the
/// connection is alive and cancel any stale-connection timeout.
Pong,
}
impl From<WatcherEvent> for Option<WsResponse> {
@@ -285,6 +291,9 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
Ok(WsRequest::Cancel) => {
let _ = chat::cancel_chat(&ctx.state);
}
Ok(WsRequest::Ping) => {
let _ = tx.send(WsResponse::Pong);
}
_ => {}
}
}
@@ -305,6 +314,9 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
Ok(WsRequest::Cancel) => {
let _ = chat::cancel_chat(&ctx.state);
}
Ok(WsRequest::Ping) => {
let _ = tx.send(WsResponse::Pong);
}
Ok(WsRequest::PermissionResponse { .. }) => {
// Permission responses outside an active chat are ignored.
}
@@ -385,6 +397,13 @@ mod tests {
assert!(matches!(req, WsRequest::Cancel));
}
#[test]
fn deserialize_ping_request() {
let json = r#"{"type": "ping"}"#;
let req: WsRequest = serde_json::from_str(json).unwrap();
assert!(matches!(req, WsRequest::Ping));
}
#[test]
fn deserialize_permission_response_approved() {
let json = r#"{
@@ -538,6 +557,13 @@ mod tests {
assert_eq!(json["type"], "agent_config_changed");
}
#[test]
fn serialize_pong_response() {
let resp = WsResponse::Pong;
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pong");
}
#[test]
fn serialize_permission_request_response() {
let resp = WsResponse::PermissionRequest {