Files
galaxy/crates/remote_server/src/protocol_tests.rs
T

282 lines
10 KiB
Rust

use prost::Message;
use super::*;
use crate::proto::{
client_message, remote_skill_proto, server_message, session_scoped_request,
BundledSkillMetadata, ClientMessage, HomeSkillMetadata, Initialize, InitializeResponse,
RemoteAgentContextSnapshot, RemoteContextFileProto, RemoteSkillProto, ServerMessage,
};
#[tokio::test]
async fn round_trip_client_message() {
let msg = ClientMessage::session_scoped(
"test-123".to_string(),
session_scoped_request::Message::Initialize(Initialize {
auth_token: String::new(),
user_id: String::new(),
user_email: String::new(),
crash_reporting_enabled: true,
codebase_index_limits: None,
}),
);
let mut buf = Vec::new();
write_client_message(&mut buf, &msg).await.unwrap();
let mut cursor = &buf[..];
let decoded: ClientMessage = read_client_message(&mut cursor).await.unwrap();
assert_eq!(decoded.request_id, "test-123");
match decoded.message {
Some(client_message::Message::SessionScoped(_)) => {}
other => panic!("unexpected message variant: {other:?}"),
}
}
#[tokio::test]
async fn round_trip_remote_agent_context_snapshot() {
let mut buf = Vec::new();
write_server_message(
&mut buf,
&ServerMessage {
request_id: String::new(),
message: Some(server_message::Message::RemoteAgentContextSnapshot(
RemoteAgentContextSnapshot {
revision: 7,
home_dir: "/home/user".to_string(),
skills: vec![
RemoteSkillProto {
path: "/bundled/pr-comments/SKILL.md".to_string(),
content: "bundled content".to_string(),
source: Some(remote_skill_proto::Source::Bundled(
BundledSkillMetadata {
id: "pr-comments".to_string(),
requires_mcp: Some("figma".to_string()),
},
)),
},
RemoteSkillProto {
path: "/home/user/.agents/skills/test/SKILL.md".to_string(),
content: "home skill content".to_string(),
source: Some(remote_skill_proto::Source::Home(HomeSkillMetadata {})),
},
],
global_rules: vec![RemoteContextFileProto {
path: "/home/user/.agents/AGENTS.md".to_string(),
content: "rule content".to_string(),
}],
},
)),
},
)
.await
.unwrap();
let decoded = read_server_message(&mut &buf[..]).await.unwrap();
match decoded.message {
Some(server_message::Message::RemoteAgentContextSnapshot(snapshot)) => {
assert_eq!(snapshot.revision, 7);
assert_eq!(snapshot.home_dir, "/home/user");
assert_eq!(snapshot.skills.len(), 2);
let Some(remote_skill_proto::Source::Bundled(bundled)) =
snapshot.skills[0].source.as_ref()
else {
panic!("expected bundled skill source");
};
assert_eq!(bundled.id, "pr-comments");
assert_eq!(bundled.requires_mcp.as_deref(), Some("figma"));
assert!(matches!(
snapshot.skills[1].source,
Some(remote_skill_proto::Source::Home(_))
));
assert_eq!(snapshot.skills[1].content, "home skill content");
assert_eq!(
snapshot.global_rules[0].path,
"/home/user/.agents/AGENTS.md"
);
assert_eq!(snapshot.global_rules[0].content, "rule content");
}
other => panic!("unexpected message variant: {other:?}"),
}
}
#[tokio::test]
async fn round_trip_server_message() {
let msg = ServerMessage {
request_id: "resp-456".to_string(),
message: Some(server_message::Message::InitializeResponse(
InitializeResponse {
server_version: "0.1.0".to_string(),
host_id: "test-host".to_string(),
},
)),
};
let mut buf = Vec::new();
write_server_message(&mut buf, &msg).await.unwrap();
let mut cursor = &buf[..];
let decoded: ServerMessage = read_server_message(&mut cursor).await.unwrap();
assert_eq!(decoded.request_id, "resp-456");
match decoded.message {
Some(server_message::Message::InitializeResponse(resp)) => {
assert_eq!(resp.server_version, "0.1.0");
assert_eq!(resp.host_id, "test-host");
}
other => panic!("unexpected message variant: {other:?}"),
}
}
#[tokio::test]
async fn read_unexpected_eof_on_empty_input() {
let mut cursor: &[u8] = &[];
let result = read_client_message(&mut cursor).await;
assert!(matches!(result, Err(ProtocolError::UnexpectedEof)));
}
#[tokio::test]
async fn read_truncated_payload() {
// Write a length prefix claiming 100 bytes, but only provide 4.
let mut buf = Vec::new();
buf.extend_from_slice(&100u32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
let mut cursor = &buf[..];
let result = read_client_message(&mut cursor).await;
assert!(matches!(result, Err(ProtocolError::UnexpectedEof)));
}
#[tokio::test]
async fn round_trip_zero_length_message() {
// A default ClientMessage with no fields set encodes to zero bytes.
let msg = ClientMessage::default();
let mut buf = Vec::new();
write_client_message(&mut buf, &msg).await.unwrap();
// The first 4 bytes should be the length (0).
assert_eq!(&buf[..4], &0u32.to_le_bytes());
let mut cursor = &buf[..];
let decoded: ClientMessage = read_client_message(&mut cursor).await.unwrap();
assert_eq!(decoded.request_id, "");
assert!(decoded.message.is_none());
}
#[tokio::test]
async fn read_message_too_large() {
// Write a length prefix exceeding MAX_MESSAGE_SIZE.
let oversized_len = (MAX_MESSAGE_SIZE as u32) + 1;
let buf = oversized_len.to_le_bytes();
let mut cursor = &buf[..];
let result = read_client_message(&mut cursor).await;
assert!(matches!(result, Err(ProtocolError::MessageTooLarge { .. })));
}
#[tokio::test]
async fn write_message_too_large() {
// Build a ClientMessage whose encoded size exceeds MAX_MESSAGE_SIZE.
let msg = ClientMessage {
request_id: "x".repeat(MAX_MESSAGE_SIZE + 1),
message: None,
};
let mut buf = Vec::new();
let result = write_client_message(&mut buf, &msg).await;
assert!(matches!(result, Err(ProtocolError::MessageTooLarge { .. })));
// Nothing should have been written to the stream.
assert!(buf.is_empty());
}
#[test]
fn try_extract_request_id_from_valid_message() {
let msg = ClientMessage::session_scoped(
"abc-123".to_string(),
session_scoped_request::Message::Initialize(Initialize {
auth_token: String::new(),
user_id: String::new(),
user_email: String::new(),
crash_reporting_enabled: true,
codebase_index_limits: None,
}),
);
let buf = msg.encode_to_vec();
assert_eq!(try_extract_request_id(&buf), Some("abc-123".to_string()));
}
#[test]
fn try_extract_request_id_from_corrupted_payload_with_valid_id() {
// Manually construct bytes: valid request_id field followed by
// corrupt trailing bytes (unterminated varint that would crash
// a full prost decode but doesn't affect our field-1 extraction).
let mut buf = Vec::new();
// Field 1 (string): tag=0x0a, length=7, "req-456"
buf.push(0x0a);
buf.push(7);
buf.extend_from_slice(b"req-456");
// Corrupt trailing bytes: unterminated varint (all continuation bits set).
buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
// request_id should still be extractable despite trailing corruption.
assert_eq!(try_extract_request_id(&buf), Some("req-456".to_string()));
}
#[test]
fn try_extract_request_id_from_empty_bytes() {
assert_eq!(try_extract_request_id(&[]), None);
}
#[test]
fn try_extract_request_id_from_garbage_bytes() {
// Completely random bytes that don't form a valid protobuf.
// This may or may not decode depending on what prost makes of it,
// but should not panic. If it decodes to an empty request_id, we
// return None.
let result = try_extract_request_id(&[0xFF, 0xFF, 0xFF, 0xFF]);
// We don't assert a specific value — just that it doesn't panic.
// If prost happens to decode something, it'll be empty or garbage.
let _ = result;
}
#[tokio::test]
async fn decode_error_extracts_request_id() {
// Construct a corrupted message with a valid request_id field.
let mut payload = Vec::new();
// Field 1 (string): tag=0x0a, length=6, "req-42"
payload.push(0x0a);
payload.push(6);
payload.extend_from_slice(b"req-42");
// Invalid trailing bytes that cause prost decode failure.
payload.extend_from_slice(&[0x0F, 0x01]);
let mut buf = Vec::new();
buf.extend_from_slice(&(payload.len() as u32).to_le_bytes());
buf.extend_from_slice(&payload);
let mut cursor = &buf[..];
let result = read_client_message(&mut cursor).await;
match result {
Err(ProtocolError::Decode(_, Some(id))) => {
assert_eq!(id.to_string(), "req-42");
}
other => panic!("expected Decode error with request_id, got: {other:?}"),
}
}
#[tokio::test]
async fn decode_error_none_when_no_request_id() {
// Completely invalid protobuf bytes with no valid field 1.
let garbage = vec![0xFF, 0xFE, 0xFD, 0xFC];
let mut buf = Vec::new();
buf.extend_from_slice(&(garbage.len() as u32).to_le_bytes());
buf.extend_from_slice(&garbage);
let mut cursor = &buf[..];
let result = read_client_message(&mut cursor).await;
match result {
Err(ProtocolError::Decode(_, None)) => {}
other => panic!("expected Decode error with None request_id, got: {other:?}"),
}
}